Bash: stwórz anonimowy fifo

38

Wszyscy wiemy mkfifoi rurociągi. Pierwszy tworzy nazwaną potok, dlatego należy wybrać nazwę, najprawdopodobniej za pomocą, mktempa później pamiętać, aby rozłączyć. Drugi tworzy anonimową potok, bez kłopotów z nazwami i usuwaniem, ale końce potoku są powiązane z poleceniami w potoku, nie jest tak naprawdę wygodne, aby jakoś uchwycić deskryptory plików i użyć ich w pozostałej części skryptu. W skompilowanym programie po prostu bym to zrobił ret=pipe(filedes); w Bash jest exec 5<>filewięc coś, czego można się spodziewać "exec 5<> -"lub czy "pipe <5 >6"jest coś takiego w Bash?

Adrian Panasiuk
źródło

Odpowiedzi:

42

Możesz odłączyć nazwany potok natychmiast po dołączeniu go do bieżącego procesu, co praktycznie skutkuje anonimowym potokiem:

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

Jeśli naprawdę chcesz uniknąć nazwanych potoków (np. System plików jest tylko do odczytu), działa również pomysł „obejrzyj deskryptory plików”. Zauważ, że jest to specyficzne dla Linuksa ze względu na użycie 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<&-
htamas
źródło
Możesz to połączyć z automatycznym wyszukiwaniem nieużywanych deskryptorów plików: stackoverflow.com/questions/8297415/…
CMCDragonkai
23

Chociaż żadna ze znanych mi powłok nie może tworzyć rur bez rozwidlenia, niektóre mają lepsze niż podstawowy potok powłoki.

W bash, ksh i zsh, zakładając, że twój system obsługuje /dev/fd(większość obecnie robi), możesz powiązać dane wejściowe lub wyjściowe polecenia z nazwą pliku: <(command)rozwija się do nazwy pliku, która oznacza potok podłączony do wyjścia commandi >(command)rozwija do nazwy pliku oznaczającej potok podłączony do wejścia command. Ta funkcja nosi nazwę zastępowania procesu . Jego głównym celem jest potokowanie więcej niż jednego polecenia do lub z drugiego, np.

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

Jest to również przydatne w walce z niektórymi wadami podstawowych rur osłonowych. Na przykład command2 < <(command1)jest równoważne command1 | command2, z wyjątkiem tego, że ma status command2. Innym przypadkiem użycia jest exec > >(postprocessing), co jest równoważne, ale bardziej czytelne niż, umieszczenie całej reszty skryptu w środku { ... } | postprocessing.

Gilles „SO- przestań być zły”
źródło
Próbowałem tego z diff i działało, ale z kdiff3 lub emacs nie działało. Domyślam się, że plik tymczasowy / dev / fd jest usuwany, zanim kdiff3 go przeczyta. A może kdiff3 próbuje odczytać plik dwa razy, a potok wysyła go tylko raz?
Eyal
@Eyal Przy utrzymywaniu procesu nazwa pliku jest „magicznym” odniesieniem do potoku (lub pliku tymczasowego w wariantach Uniksa, które nie obsługują tych magicznych wariantów). Sposób implementacji magii zależy od systemu operacyjnego. Linux implementuje je jako „magiczne” dowiązania symboliczne, których celem nie jest poprawna nazwa pliku (coś w tym rodzaju pipe:[123456]). Emacs widzi, że celem dowiązania symbolicznego nie jest istniejąca nazwa pliku, co dezorientuje go na tyle, że nie czyta pliku (może istnieć opcja, aby i tak go odczytał, chociaż Emacs nie lubi otwierać potoku jako pliku plik mimo to).
Gilles „SO- przestań być zły”
10

Bash 4 ma koprocesy .

Koproces wykonywany jest asynchronicznie w podpowłoce, tak jakby polecenie zostało zakończone za pomocą operatora sterującego „&”, z dwukierunkowym potokiem ustanowionym między wykonującą powłoką a koprocesem.

Format koprocesu to:

coproc [NAME] command [redirections] 
Wstrzymano do odwołania.
źródło
3

Według stanu na październik 2012 r. Ta funkcjonalność nadal nie istnieje w Bash, ale można użyć coproc, jeśli potrzebujesz tylko nienazwanych / anonimowych potoków, aby porozmawiać z procesem potomnym. Problem z coproc w tym momencie polega na tym, że najwyraźniej obsługiwany jest tylko jeden na raz. Nie mogę zrozumieć, dlaczego Coproc ma takie ograniczenie. Powinny one być ulepszeniem istniejącego kodu tła zadań (& &), ale to jest pytanie do autorów bash.

Radu C.
źródło
Obsługiwany jest nie tylko jeden koproces. Możesz je nazwać, o ile nie podasz prostego polecenia. Zamiast tego podaj mu listę poleceń: coproc THING { dothing; }teraz masz już swoje FD ${THING[*]}i możesz je uruchamiać, coproc OTHERTHING { dothing; }wysyłać i odbierać rzeczy do iz obu.
clacke
2
@clacke in man bash, pod tytułem BŁĘDY, mówią tak: Może być tylko jeden aktywny koproces na raz . I pojawi się ostrzeżenie, jeśli uruchomisz drugi coproc. Wygląda na to, że działa, ale nie wiem, co wybucha w tle.
Radu C
Ok, więc obecnie działa tylko na szczęście, nie dlatego, że był celowy. Uczciwe ostrzeżenie, dzięki. :-)
clacke
2

Choć @ DavidAnderson za odpowiedź obejmuje wszystkie podstawy i oferuje jedne zabezpieczeniach ładne, najważniejszą rzeczą jest to, że ujawnia się w swoje ręce anonimowego rury jest tak proste, jak <(:), jak długo pozostanie w systemie Linux.

Zatem najkrótsza i najprostsza odpowiedź na twoje pytanie to:

exec 5<> <(:)

W systemie macOS to nie zadziała, wtedy musisz utworzyć tymczasowy katalog, w którym mieści się nazwany fifo, dopóki nie przekierujesz do niego. Nie wiem o innych BSD.

clacke
źródło
Zdajesz sobie sprawę, że twoja odpowiedź działa tylko z powodu błędu w systemie Linux. Ten błąd nie istnieje w systemie macOS, dlatego wymaga bardziej złożonego rozwiązania. Ostateczna wersja, którą opublikowałem, będzie działać w systemie Linux, nawet jeśli błąd w systemie Linux zostanie naprawiony.
David Anderson
@DavidAnderson Wygląda na to, że masz o tym większą wiedzę niż ja. Dlaczego zachowanie Linuksa jest błędem?
clacke
1
Jeśli execzostanie przekazany anonimowy plik fifo, który jest otwierany tylko do odczytu, execnie powinien zezwalać na otwieranie tego anonimowego pliku fifo do odczytu i zapisu przy użyciu niestandardowego deskryptora pliku. Powinieneś spodziewać się -bash: /dev/fd/5: Permission deniedkomunikatu, który właśnie powoduje problemy z systemem macOS. Uważam, że błąd polega na tym, że Ubuntu nie wyświetla tego samego komunikatu. Byłbym skłonny zmienić zdanie, gdyby ktoś był w stanie przedstawić dokumentację stwierdzającą, że exec 5<> <(:)jest to dozwolone.
David Anderson
@DavidAnderson Wow, to fascynujące. Zakładałem, że bash robi coś wewnętrznie, ale okazuje się, że jest to Linux, który pozwala po prostu działać open(..., O_RDWR)na jednym jednokierunkowym końcu potoku zapewnianym przez podstawienie i zamienia go w potok dwukierunkowy w jednym FD. Prawdopodobnie masz rację, że nie należy na tym polegać. :-D Dane wyjściowe z użyciem programu uruchamiającego do utworzenia potoku, a następnie zmiany jego przeznaczenia za pomocą bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke
Nie ma to znaczenia, ale jeśli chcesz zobaczyć w Ubuntu, co jest przekazywane exec 5<>, wpisz fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson
1

Poniższa funkcja została przetestowana przy użyciu GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). System operacyjny to Ubuntu 18. Ta funkcja przyjmuje jeden parametr, który jest pożądanym deskryptorem pliku dla anonimowego FIFO.

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
}

Poniższa funkcja została przetestowana przy użyciu GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). System operacyjny to macOS High Sierra. Ta funkcja zaczyna się od utworzenia nazwanego FIFO w katalogu tymczasowym znanym tylko procesowi, który go utworzył . Następnie deskryptor pliku zostaje przekierowany do FIFO. Na koniec FIFO jest odłączany od nazwy pliku poprzez usunięcie katalogu tymczasowego. To sprawia, że ​​FIFO jest anonimowy.

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

Powyższe funkcje można połączyć w jedną funkcję, która będzie działać w obu systemach operacyjnych. Poniżej znajduje się przykład takiej funkcji. Tutaj podjęto próbę stworzenia prawdziwie anonimowego FIFO. Jeśli się to nie powiedzie, wówczas nazwany FIFO zostanie utworzony i przekształcony w anonimowy 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
        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"; }
}

Oto przykład tworzenia anonimowego FIFO, a następnie pisania tekstu do tego samego FIFO.

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

Poniżej znajduje się przykład czytania całej zawartości anonimowego FIFO.

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

Daje to następujący wynik.

Now is the
time for all
good men

Poniższe polecenie zamyka anonimowy FIFO.

eval exec "$fd>&-"

Odnośniki:
Tworzenie anonimowego potoku do późniejszego wykorzystania
Pliki w publicznie zapisywanych katalogach są niebezpiecznym
zabezpieczeniem skryptów powłoki

David Anderson
źródło
0

Korzystając ze świetnej i jasnej odpowiedzi od htamów, zmodyfikowałem ją trochę, aby użyć jej w jednej linijce, oto ona:

# 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<&-
Luiz Felipe Silva
źródło
7
Nie mogę nie zauważyć, że twój liniowiec ma więcej niż jedną linię.
Dmitrij Grigoryev