Jak możesz porównać dwa potoki w Bash?

143

Jak możesz porównać dwa potoki bez używania plików tymczasowych w Bash? Powiedzmy, że masz dwa potoki poleceń:

foo | bar
baz | quux

I chcesz znaleźć diffich wyniki. Jednym z rozwiązań byłoby oczywiście:

foo | bar > /tmp/a
baz | quux > /tmp/b
diff /tmp/a /tmp/b

Czy można to zrobić bez użycia plików tymczasowych w Bash? Możesz pozbyć się jednego pliku tymczasowego, podłączając jeden z potoków do diff:

foo | bar > /tmp/a
baz | quux | diff /tmp/a -

Ale nie można jednocześnie potokować obu potoków do diff (przynajmniej nie w oczywisty sposób). Czy jest jakaś sprytna sztuczka polegająca /dev/fdna zrobieniu tego bez używania plików tymczasowych?

Adam Rosenfield
źródło

Odpowiedzi:

146

Jednowierszowy z 2 plikami tmp (nie tym, czego chcesz) byłby:

 foo | bar > file1.txt && baz | quux > file2.txt && diff file1.txt file2.txt

W przypadku basha możesz jednak spróbować:

 diff <(foo | bar) <(baz | quux)

 foo | bar | diff - <(baz | quux)  # or only use process substitution once

Druga wersja będzie wyraźniej przypominać ci, które wejście było które, pokazując
-- /dev/stdinvs. ++ /dev/fd/63lub coś, zamiast dwóch ponumerowanych fds.


Nawet nazwany potok nie pojawi się w systemie plików, przynajmniej w systemach operacyjnych, w których bash może zaimplementować podstawianie procesów przy użyciu nazw plików, takich jak /dev/fd/63uzyskanie nazwy pliku, którą polecenie może otworzyć i odczytać, aby faktycznie czytać z już otwartego deskryptora pliku ustawionego przez bash up przed wykonaniem polecenia. (tj. bash używa pipe(2)przed rozwidleniem, a następnie dup2przekierowuje z wyjścia quuxdo deskryptora pliku wejściowego dla diff, na fd 63.)

W systemie bez "magicznych" /dev/fdlub /proc/self/fd, bash może używać nazwanych potoków do implementacji podstawiania procesów, ale przynajmniej sam zarządzałby nimi, w przeciwieństwie do plików tymczasowych, a twoje dane nie zostałyby zapisane w systemie plików.

Możesz sprawdzić, jak bash implementuje podstawianie za pomocą, echo <(true)aby wypisać nazwę pliku zamiast czytać z niego. Drukuje /dev/fd/63na typowym systemie Linux. Aby uzyskać więcej informacji na temat dokładnie tego, jakich wywołań systemowych używa bash, to polecenie w systemie Linux będzie śledzić wywołania systemowe plików i deskryptorów plików

strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'

Bez basha możesz stworzyć nazwaną potokę . Służy -do mówienia, diffaby odczytać jedno wejście ze STDIN i użyj nazwanego potoku jako drugiego:

mkfifo file1_pipe.txt
foo|bar > file1_pipe.txt && baz | quux | diff file1_pipe.txt - && rm file1_pipe.txt

Zauważ, że możesz przesłać tylko jedno wyjście do wielu wejść za pomocą polecenia tee:

ls *.txt | tee /dev/tty txtlist.txt 

Powyższe polecenie wyświetla wyjście ls * .txt na terminal i wyprowadza je do pliku tekstowego txtlist.txt.

Ale dzięki podstawianiu procesów możesz użyć teedo wprowadzenia tych samych danych do wielu potoków:

cat *.txt | tee >(foo | bar > result1.txt)  >(baz | quux > result2.txt) | foobar
VonC
źródło
5
nawet bez basha, możesz użyć tymczasowych gier fifomkfifo a; cmd >a& cmd2|diff a -; rm a
unhammer
Można użyć zwykłej rury dla jednego z args: pipeline1 | diff -u - <(pipeline2). Wtedy wynik będzie wyraźniej przypominał ci, które wejście było które, pokazując -- /dev/stdinvs. ++ /dev/fd/67lub coś, zamiast dwóch ponumerowanych fds.
Peter Cordes,
substitution ( foo <( pipe )) procesu nie modyfikuje systemu plików. Fajka jest anonimowa ; nie ma nazwy w systemie plików . Powłoka używa pipewywołania systemowego do jej utworzenia, a nie mkfifo. Służy strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'do śledzenia wywołań systemowych plików i deskryptorów plików, jeśli chcesz się o tym przekonać. W systemie Linux /dev/fd/63jest częścią /procwirtualnego systemu plików; automatycznie zawiera wpisy dla każdego deskryptora pliku i nie jest kopią zawartości. Więc nie możesz nazwać tego „plikiem tymczasowym”, chyba że się foo 3<bar.txtliczy
Peter Cordes
@PeterCordes Zalety. W odpowiedzi zawarłem Twój komentarz, aby uzyskać lepszą widoczność.
VonC
1
@PeterCordes Wszelkie zmiany pozostawiam tobie: to właśnie sprawia, że ​​Stack Overflow jest interesujący: każdy może „naprawić” odpowiedź.
VonC
127

W bash możesz użyć podpowłok, aby wykonać potoki poleceń indywidualnie, zamykając potok w nawiasach. Możesz następnie poprzedzić je <, aby utworzyć anonimowe potoki nazwane, które następnie możesz przekazać do diff.

Na przykład:

diff <(foo | bar) <(baz | quux)

Anonimowe potoki nazwane są zarządzane przez bash, więc są tworzone i niszczone automatycznie (w przeciwieństwie do plików tymczasowych).

BenM
źródło
1
O wiele bardziej szczegółowe niż moja redakcja dotycząca tego samego rozwiązania - anonimowej partii -. +1
VonC
4
Nazywa się to zastępowaniem procesów w Bash.
Franklin Yu,
5

Niektórzy ludzie wchodzący na tę stronę mogą szukać różnicy wiersz po wierszu, dla którego commlub grep -fnależy go użyć zamiast tego.

Należy zwrócić uwagę na to, że we wszystkich przykładach odpowiedzi różnice nie zaczną się tak naprawdę, dopóki oba strumienie nie zakończą się. Przetestuj to na przykład:

comm -23 <(seq 100 | sort) <(seq 10 20 && sleep 5 && seq 20 30 | sort)

Jeśli jest to problem, możesz wypróbować sd (stream diff), który nie wymaga sortowania (tak jak w przypadku comm) ani zastępowania procesu, jak w powyższych przykładach, jest o rząd wielkości szybszy niż grep -f i obsługuje nieskończone strumienie.

Przykład testowy, który proponuję, byłby napisany w następujący sposób sd:

seq 100 | sd 'seq 10 20 && sleep 5 && seq 20 30'

Ale różnica polega na tym, seq 100że od razu się to zmieni seq 10. Zwróć uwagę, że jeśli jednym ze strumieni jest a tail -f, nie można wykonać różnicy z podstawieniem procesu.

Oto post na blogu, który napisałem o różnicowaniu strumieni na terminalu, który wprowadza sd.

mlg
źródło