Bash: crea Fifo anonimo


38

Sappiamo tutti mkfifoe condutture. Il primo crea una pipa denominata , quindi è necessario selezionare un nome, molto probabilmente con mktempe in seguito ricordare di scollegare. L'altro crea una pipe anonima, senza problemi con i nomi e la rimozione, ma le estremità della pipe si legano ai comandi nella pipeline, non è davvero conveniente in qualche modo afferrare i descrittori di file e usarli nel resto della sceneggiatura. In un programma compilato, lo farei semplicemente ret=pipe(filedes); in Bash c'è exec 5<>filequalcosa che ci si aspetta "exec 5<> -"o "pipe <5 >6"qualcosa del genere in Bash?

Risposte:


42

È possibile scollegare una pipa denominata immediatamente dopo averla collegata al processo corrente, che praticamente risulta in una pipa anonima:

# create a temporary named pipe
PIPE=$(mktemp -u)
mkfifo $PIPE
# attach it to file descriptor 3
exec 3<>$PIPE
# unlink the named pipe
rm $PIPE
...
# anything we write to fd 3 can be read back from it
echo 'Hello world!' >&3
head -n1 <&3
...
# close the file descriptor when we are finished (optional)
exec 3>&-

Se vuoi davvero evitare le named pipe (ad es. Il filesystem è di sola lettura), anche la tua idea di "avere un'idea dei descrittori di file" funziona. Si noti che questo è specifico di Linux a causa dell'uso di procfs.

# start a background pipeline with two processes running forever
tail -f /dev/null | tail -f /dev/null &
# save the process ids
PID2=$!
PID1=$(jobs -p %+)
# hijack the pipe's file descriptors using procfs
exec 3>/proc/$PID1/fd/1 4</proc/$PID2/fd/0
# kill the background processes we no longer need
# (using disown suppresses the 'Terminated' message)
disown $PID2
kill $PID1 $PID2
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptors when we are finished (optional)
exec 3>&- 4<&-

È possibile combinare questo con individuazione automatica dei descrittori di file inutilizzati: stackoverflow.com/questions/8297415/...
CMCDragonkai

23

Sebbene nessuno dei gusci che conosco possa creare tubi senza biforcazione, alcuni hanno una pipeline migliore rispetto a quella di base.

In bash, ksh e zsh, supponendo che il tuo sistema supporti /dev/fd(la maggior parte lo fa al giorno d'oggi), puoi collegare l'input o l'output di un comando a un nome file: si <(command)espande in un nome file che designa una pipe connessa all'output commande si >(command)espande a un nome file che designa una pipe collegata all'ingresso di command. Questa funzione è chiamata sostituzione del processo . Il suo scopo principale è quello di reindirizzare più di un comando all'interno o all'esterno di un altro, ad es.

diff <(transform <file1) <(transform <file2)
tee >(transform1 >out1) >(transform2 >out2)

Questo è utile anche per combattere alcune delle carenze dei tubi shell di base. Ad esempio, command2 < <(command1)è equivalente a command1 | command2, tranne per il fatto che il suo stato è quello di command2. Un altro caso d'uso è exec > >(postprocessing), che equivale a, ma più leggibile di, mettendo dentro tutto il resto dello script { ... } | postprocessing.


L'ho provato con diff e ha funzionato ma con kdiff3 o con emacs non ha funzionato. La mia ipotesi è che il file temporaneo / dev / fd venga rimosso prima che kdiff3 riesca a leggerlo. O forse kdiff3 sta provando a leggere il file due volte e la pipe lo sta inviando una sola volta?
Eyal,

@Eyal Con il processo di approvvigionamento, il nome del file è un riferimento "magico" a una pipe (o un file temporaneo su varianti Unix che non supportano queste varianti magiche). Come viene implementata la magia dipende dal sistema operativo. Linux li implementa come collegamenti simbolici "magici" il cui obiettivo non è un nome file valido (è qualcosa di simile pipe:[123456]). Emacs vede che la destinazione del link simbolico non è un nome file esistente e che lo confonde abbastanza da non leggere il file (potrebbe esserci un'opzione per farlo leggere comunque, anche se a Emacs non piace aprire una pipe come file comunque).
Gilles 'SO- smetti di essere malvagio' il

10

Bash 4 ha coprocessi .

Un coprocesso viene eseguito in modo asincrono in una subshell, come se il comando fosse stato terminato con l'operatore '&' control, con una pipe a due vie stabilita tra la shell in esecuzione e il coprocesso.

Il formato per un coprocesso è:

coproc [NAME] command [redirections] 

3

A partire da ottobre 2012 questa funzionalità non sembra ancora esistere in Bash, ma coproc può essere usato se tutto ciò di cui hai bisogno per pipe anonime / anonime è parlare con un processo figlio. Il problema con coproc a questo punto è che apparentemente è supportato solo uno alla volta. Non riesco a capire perché Coproc abbia questa limitazione. Avrebbero dovuto essere un miglioramento del codice di background dell'attività esistente (il & op), ma questa è una domanda per gli autori di bash.


Non è supportato solo un coprocesso. Puoi nominarli, purché tu non fornisca un semplice comando. Dagli invece un elenco di comandi: coproc THING { dothing; }ora i tuoi FD sono presenti ${THING[*]}e puoi eseguire, coproc OTHERTHING { dothing; }inviare e ricevere cose da e verso entrambi.
Clacke

2
@clacke man bash, sotto il titolo BUGS, dicono questo: Potrebbe esserci un solo coprocesso attivo alla volta . E ricevi un avviso se avvii un secondo coprocessore. Sembra funzionare, ma non so cosa esploda in background.
Radu C,

Ok, quindi attualmente funziona solo per fortuna, non perché era intenzionale. Attenzione, grazie. :-)
clacke il

2

Mentre la risposta di @ DavidAnderson copre tutte le basi e offre alcune belle garanzie, la cosa più importante che rivela è che mettere le mani su una pipa anonima è facile come <(:), purché rimanga su Linux.

Quindi la risposta più breve e semplice alla tua domanda è:

exec 5<> <(:)

Su macOS non funzionerà, quindi dovrai creare una directory temporanea per ospitare il Fifo denominato fino a quando non ti verrà reindirizzato ad esso. Non conosco altri BSD.


Ti rendi conto che la tua risposta funziona solo a causa di un bug in Linux. Questo bug non esiste in macOS, quindi richiede la soluzione più complessa. La versione finale che ho pubblicato funzionerà in Linux anche se il bug in Linux è stato corretto.
David Anderson,

@DavidAnderson Sembra che tu ne abbia una conoscenza più profonda di me. Perché il comportamento di Linux è un bug?
Clacke

1
Se execviene passato e FIFO anonimo che viene aperto solo per la lettura, execnon deve consentire l'apertura di FIFO anonimo per la lettura e la scrittura utilizzando un descrittore di file personalizzato. Dovresti aspettarti di ricevere un -bash: /dev/fd/5: Permission deniedmessaggio, che è il problema di macOS. Credo che il bug sia che Ubuntu non produce lo stesso messaggio. Sarei disposto a cambiare idea se qualcuno potesse produrre documentazione dicendo che exec 5<> <(:)è esplicitamente consentito.
David Anderson,

@DavidAnderson Wow, è affascinante. Supponevo che bash stesse facendo qualcosa internamente, ma si scopre che è Linux che permette semplicemente di fare open(..., O_RDWR)su un terminale unidirezionale fornito dalla sostituzione e che lo trasforma in un tubo bidirezionale in un FD. Probabilmente hai ragione sul fatto che non si dovrebbe fare affidamento su questo. Uscita :-D usare piperw di execline per creare il tubo, poi riproporre con bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke

Non è importante, ma se vuoi vedere in Ubuntu a cosa viene passato exec 5<>, allora entra fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson,

1

La seguente funzione è stata testata utilizzando GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). Il sistema operativo era Ubuntu 18. Questa funzione accetta un singolo parametro che è il descrittore di file desiderato per il FIFO anonimo.

MakeFIFO() {
    local "MakeFIFO_upper=$(ulimit -n)" 
    if [[ $# -ne 1 || ${#1} -gt ${#MakeFIFO_upper} || -n ${1%%[0-9]*} || 10#$1 -le 2
        || 10#$1 -ge MakeFIFO_upper ]] || eval ! exec "$1<> " <(:) 2>"/dev/null"; then
        echo "$FUNCNAME: $1: Could not create FIFO" >&2
        return "1"
    fi
}

La seguente funzione è stata testata utilizzando GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). Il sistema operativo era macOS High Sierra. Questa funzione inizia creando un FIFO denominato in una directory temporanea nota solo al processo che l'ha creata . Successivamente, il descrittore di file viene reindirizzato a FIFO. Infine, FIFO viene scollegato dal nome del file eliminando la directory temporanea. Ciò rende anonimo FIFO.

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
        MakeFIFO_file="$MakeFIFO_directory/pipe"
        mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
        ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

Le funzioni di cui sopra possono essere combinate con un'unica funzione che funzionerà su entrambi i sistemi operativi. Di seguito è riportato un esempio di tale funzione. Qui, si tenta di creare un FIFO veramente anonimo. In caso di esito negativo, viene creato e convertito un FIFO denominato in un FIFO anonimo.

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        if eval ! exec "$1<> " <(:) 2>"/dev/null"; then
            MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
            MakeFIFO_file="$MakeFIFO_directory/pipe"
            mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
            ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        fi
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

Ecco un esempio di creazione di un FIFO anonimo, quindi della scrittura di un testo nello stesso FIFO.

fd="6"
MakeFIFO "$fd"
echo "Now is the" >&"$fd"
echo "time for all" >&"$fd"
echo "good men" >&"$fd"

Di seguito è riportato un esempio di lettura dell'intero contenuto della FIFO anonima.

echo "EOF" >&"$fd"
while read -u "$fd" message; do
    [[ $message != *EOF ]] || break
    echo "$message"
done

Questo produce il seguente output.

Now is the
time for all
good men

Il comando seguente chiude la FIFO anonima.

eval exec "$fd>&-"

Riferimenti: la
creazione di una pipe anonima per uso futuro I
file nelle directory scrivibili pubblicamente sono pericolosi
Shell Security Security


0

Usando la risposta grande e brillante di htama, l'ho modificato un po 'per usarlo in una fodera, eccolo qui:

# create a temporary named pipe
PIPE=(`(exec 0</dev/null 1</dev/null; (( read -d \  e < /proc/self/stat ; echo $e >&2 ; exec tail -f /dev/null 2> /dev/null ) | ( read -d \  e < /proc/self/stat ; echo $e  >&2 ; exec tail -f /dev/null 2> /dev/null )) &) 2>&1 | for ((i=0; i<2; i++)); do read e; printf "$e "; done`)
# attach it to file descriptors 3 and 4
exec 3>/proc/${PIPE[0]}/fd/1 4</proc/${PIPE[1]}/fd/0
...
# kill the temporary pids
kill ${PIPE[@]}
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptor when we are finished (optional)
exec 3>&- 4<&-

7
Non posso fare a meno di notare che il tuo one-liner ha più di una riga.
Dmitry Grigoryev,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.