Bash: Erstelle anonymes Fifo


38

Wir alle kennen mkfifound Pipelines. Der erste erzeugt eine Named Pipe, daher muss man einen Namen auswählen, wahrscheinlich mit mktempund später daran denken, die Verknüpfung zu lösen. Der andere erzeugt eine anonyme Pipe, ohne Probleme mit Namen und Entfernung, aber die Enden der Pipe werden mit den Befehlen in der Pipeline verknüpft. Es ist nicht sehr praktisch, die Dateideskriptoren in den Griff zu bekommen und sie im Rest zu verwenden des Skripts. In einem kompilierten Programm würde ich einfach tun ret=pipe(filedes); In Bash gibt es exec 5<>fileso etwas wie "exec 5<> -"oder "pipe <5 >6"gibt es so etwas in Bash?

Antworten:


42

Sie können die Verknüpfung einer Named Pipe unmittelbar nach dem Anhängen an den aktuellen Prozess aufheben, was praktisch zu einer anonymen Pipe führt:

# 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>&-

Wenn Sie Named Pipes wirklich vermeiden möchten (z. B. das Dateisystem ist schreibgeschützt), funktioniert auch Ihre Idee, die Dateideskriptoren in den Griff zu bekommen. Beachten Sie, dass dies aufgrund der Verwendung von procfs Linux-spezifisch ist.

# 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<&-

Sie können dies mit der automatischen Suche nach nicht verwendeten Dateideskriptoren kombinieren: stackoverflow.com/questions/8297415/…
CMCDragonkai

23

Während keine der Muscheln, die ich kenne, Rohre ohne Gabelung herstellen kann, haben einige eine bessere als die grundlegende Muschelrohrleitung.

In bash, ksh und zsh können Sie, sofern Ihr System dies unterstützt /dev/fd(die meisten heutzutage), die Eingabe oder Ausgabe eines Befehls an einen Dateinamen binden: <(command)Wird zu einem Dateinamen erweitert, der eine Pipe bezeichnet, die mit der Ausgabe von verbunden ist command, und wird >(command)erweitert auf einen Dateinamen, der eine Pipe bezeichnet, die mit dem Eingang von verbunden ist command. Diese Funktion wird als Prozessersetzung bezeichnet . Sein Hauptzweck besteht darin, mehr als einen Befehl in einen anderen oder aus einem anderen heraus zu leiten, z.

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

Dies ist auch nützlich, um einige der Mängel grundlegender Mantelrohre zu bekämpfen. Zum Beispiel command2 < <(command1)ist äquivalent zu command1 | command2, außer dass sein Status der von ist command2. Ein weiterer Anwendungsfall ist exec > >(postprocessing), dass er dem Rest des Skripts entspricht, aber besser lesbar ist, als es zu tun { ... } | postprocessing.


Ich habe das mit diff versucht und es hat funktioniert, aber mit kdiff3 oder mit Emacs hat es nicht funktioniert. Ich vermute, dass die temporäre Datei / dev / fd entfernt wird, bevor kdiff3 sie lesen kann. Oder vielleicht versucht kdiff3, die Datei zweimal zu lesen und die Pipe sendet sie nur einmal?
Eyal

@Eyal Bei der Prozessunterstützung ist der Dateiname ein „magischer“ Verweis auf eine Pipe (oder eine temporäre Datei bei Unix-Varianten, die diese magischen Varianten nicht unterstützen). Wie die Magie implementiert wird, hängt vom Betriebssystem ab. Linux implementiert sie als "magische" symbolische Links, deren Ziel kein gültiger Dateiname ist (so etwas wie pipe:[123456]). Emacs stellt fest, dass das Ziel des Symlinks kein vorhandener Dateiname ist und verwirrt genug, dass die Datei nicht gelesen wird (es gibt möglicherweise eine Option, die es veranlasst, die Datei trotzdem zu lesen, obwohl Emacs das Öffnen einer Pipe als nicht mag Datei trotzdem).
Gilles 'SO - hör auf, böse zu sein'

10

Bash 4 hat Koprozesse .

Ein Coprozess wird in einer Subshell asynchron ausgeführt, als ob der Befehl mit dem Steueroperator '&' beendet worden wäre, wobei eine Zweiwege-Pipe zwischen der ausführenden Shell und dem Coprozess eingerichtet wird.

Das Format für einen Coprozess ist:

coproc [NAME] command [redirections] 

3

Ab Oktober 2012 scheint diese Funktionalität in Bash noch nicht vorhanden zu sein, aber Coproc kann verwendet werden, wenn Sie nur mit einem untergeordneten Prozess sprechen müssen. Das Problem mit Coproc an dieser Stelle ist, dass anscheinend immer nur einer unterstützt wird. Ich kann nicht herausfinden, warum Coproc diese Einschränkung hat. Sie hätten eine Erweiterung des vorhandenen Hintergrundcodes für Tasks (& op) sein sollen, aber das ist eine Frage für die Autoren von bash.


Es wird nicht nur ein Coprozess unterstützt. Sie können sie benennen, solange Sie keinen einfachen Befehl angeben. Geben Sie ihm stattdessen eine Befehlsliste: coproc THING { dothing; }Jetzt befinden sich Ihre FDs in der Liste, ${THING[*]}und Sie können coproc OTHERTHING { dothing; }Dinge von und zu beiden ausführen und senden und empfangen.
Klackern Sie

2
@clacke in man bash, unter dem BUGS-Titel, heißt es: Es kann immer nur einen aktiven Coprozess geben . Und Sie erhalten eine Warnung, wenn Sie einen zweiten Coproc starten. Es scheint zu funktionieren, aber ich weiß nicht, was im Hintergrund explodiert.
Radu C

Ok, also es funktioniert momentan nur durch Glück, nicht weil es absichtlich war. Faire Warnung, danke. :-)
Klackern

2

Während die Antwort von @ DavidAnderson alle Grundlagen abdeckt und einige nette Schutzmaßnahmen bietet, ist das Wichtigste, was sich herausstellt , dass es so einfach ist, anonyme Pfeifen in die Hände zu bekommen<(:) , solange Sie unter Linux bleiben.

Die kürzeste und einfachste Antwort auf Ihre Frage lautet also:

exec 5<> <(:)

Unter macOS funktioniert es nicht. Dann müssen Sie ein temporäres Verzeichnis erstellen, in dem das benannte FIFO gespeichert wird, bis Sie es umgeleitet haben. Ich weiß nichts über andere BSDs.


Sie erkennen, dass Ihre Antwort nur aufgrund eines Linux-Fehlers funktioniert. Dieser Fehler existiert in macOS nicht und erfordert daher die komplexere Lösung. Die endgültige Version, die ich gepostet habe, wird unter Linux funktionieren, auch wenn der Fehler in Linux behoben ist.
David Anderson

@DavidAnderson Klingt so, als hättest du ein tieferes Wissen darüber als ich. Warum ist das Linux-Verhalten ein Fehler?
Klackern Sie

1
Wenn übergeben execwird und ein anonymes FIFO nur zum Lesen geöffnet wird, execdarf dieses anonyme FIFO nicht zum Lesen und Schreiben mit einem benutzerdefinierten Dateideskriptor geöffnet werden. Sie sollten damit rechnen, eine -bash: /dev/fd/5: Permission deniedNachricht zu erhalten, auf die sich macOS bezieht. Ich glaube, der Fehler ist, dass Ubuntu nicht die gleiche Nachricht erzeugt. Ich wäre bereit, meine Meinung zu ändern, wenn jemand Unterlagen vorlegen könnte, aus denen hervorgeht, dass dies exec 5<> <(:)zulässig ist.
David Anderson

@ David Anderson Wow, das ist faszinierend. Ich habe angenommen, dass bash intern etwas unternimmt, aber es hat sich herausgestellt, dass es unter Linux möglich ist, einfach open(..., O_RDWR)ein durch die Substitution bereitgestelltes unidirektionales Pipe-Ende zu bearbeiten und es in einem FD in eine bidirektionale Pipe umzuwandeln. Sie haben wahrscheinlich Recht, dass man sich nicht darauf verlassen sollte. :-D Ausgabe von execline's piperw , um die Pipe zu erstellen, und anschließende Umnutzung mit bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke

Nicht, dass es darauf ankommt, aber wenn du unter Ubuntu sehen willst, an was übergeben wird exec 5<>, dann gib ein fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson

1

Die folgende Funktion wurde mit getestet GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). Das Betriebssystem war Ubuntu 18. Diese Funktion verwendet einen einzelnen Parameter, der der gewünschte Dateideskriptor für das anonyme FIFO ist.

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
}

Die folgende Funktion wurde mit getestet GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). Das Betriebssystem war macOS High Sierra. Diese Funktion erstellt zunächst ein benanntes FIFO in einem temporären Verzeichnis, das nur dem Prozess bekannt ist, der es erstellt hat . Als nächstes wird der Dateideskriptor zum FIFO umgeleitet. Schließlich wird das FIFO vom Dateinamen getrennt, indem das temporäre Verzeichnis gelöscht wird. Dies macht den FIFO anonym.

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"; }
}

Die oben genannten Funktionen können zu einer einzigen Funktion kombiniert werden, die auf beiden Betriebssystemen funktioniert. Nachfolgend finden Sie ein Beispiel für eine solche Funktion. Hier wird versucht, einen wirklich anonymen FIFO zu erstellen. Wenn dies nicht erfolgreich ist, wird ein benannter FIFO erstellt und in einen anonymen FIFO konvertiert.

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"; }
}

Hier ist ein Beispiel für das Erstellen eines anonymen FIFOs und anschließendes Schreiben von Text in dasselbe FIFO.

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

Nachfolgend finden Sie ein Beispiel für das Lesen des gesamten Inhalts des anonymen FIFO.

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

Dies erzeugt die folgende Ausgabe.

Now is the
time for all
good men

Mit dem folgenden Befehl wird das anonyme FIFO geschlossen.

eval exec "$fd>&-"

Verweise:
Erstellen einer anonymen Pipe für die spätere Verwendung
Dateien in öffentlich beschreibbaren Verzeichnissen sind eine gefährliche
Shell-Skriptsicherheit


0

Unter Verwendung der großartigen und hellen Antwort von htamas habe ich sie ein wenig modifiziert, um sie in einem Einzeiler zu verwenden. Hier ist sie:

# 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
Ich kann nicht umhin zu bemerken , dass Ihr Einzeiler mehr als eine Zeile hat.
Dmitry Grigoryev
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.