Come si possono diff due pipeline in Bash?


143

Come si possono diff due pipeline senza usare file temporanei in Bash? Supponi di avere due pipeline di comandi:

foo | bar
baz | quux

E vuoi trovare il diffnei loro output. Una soluzione sarebbe ovviamente quella di:

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

È possibile farlo senza l'uso di file temporanei in Bash? È possibile eliminare un file temporaneo eseguendo il piping in una delle pipeline per diff:

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

Ma non è possibile convogliare entrambe le condutture in diff contemporaneamente (non in modo ovvio, almeno). C'è qualche trucco intelligente che coinvolge /dev/fdfarlo senza usare file temporanei?

Risposte:


146

Una riga con 2 file tmp (non quello che vuoi) sarebbe:

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

Con bash , potresti provare però:

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

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

La seconda versione ti ricorderà più chiaramente quale input era, mostrando
-- /dev/stdincontro ++ /dev/fd/63o qualcosa del genere, invece di due fds numerati.


Nemmeno una pipa con nome apparirà nel filesystem, almeno sui sistemi operativi in ​​cui bash può implementare la sostituzione di processo usando nomi di file come /dev/fd/63ottenere un nome file da cui il comando può aprire e leggere per leggere effettivamente da un descrittore di file già aperto che Bash imposta prima di eseguire il comando. (cioè bash usa pipe(2)prima di fork, e poi dup2per reindirizzare dall'output di quuxa un descrittore di file di input per diff, su fd 63.)

Su un sistema senza "magico" /dev/fdo /proc/self/fd, bash potrebbe usare named pipe per implementare la sostituzione dei processi, ma almeno li gestirà da solo, diversamente dai file temporanei, e i tuoi dati non verrebbero scritti nel filesystem.

Puoi controllare come bash implementa la sostituzione del processo echo <(true)per stampare il nome del file invece di leggerlo. Stampa /dev/fd/63su un tipico sistema Linux. O per maggiori dettagli su ciò che il sistema chiama bash, questo comando su un sistema Linux traccerà le chiamate di sistema con descrittori di file

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

Senza bash, potresti creare una pipa denominata . Utilizzare -per dire diffdi leggere un input da STDIN e utilizzare la pipe denominata come l'altro:

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

Si noti che è possibile reindirizzare solo un'uscita a più ingressi con il comando tee:

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

Il comando sopra mostra l'output di ls * .txt sul terminale e lo invia al file di testo txtlist.txt.

Ma con la sostituzione del processo, è possibile utilizzare teeper inserire gli stessi dati in più pipeline:

cat *.txt | tee >(foo | bar > result1.txt)  >(baz | quux > result2.txt) | foobar

5
anche senza bash, puoi usare il FIFO temporaneomkfifo a; cmd >a& cmd2|diff a -; rm a
unhammer il

È possibile utilizzare un tubo normale per uno dei args: pipeline1 | diff -u - <(pipeline2). Quindi l'output ti ricorderà più chiaramente quale input era, mostrando -- /dev/stdincontro ++ /dev/fd/67o qualcosa del genere, invece di due fds numerati.
Peter Cordes,

process substitution ( foo <( pipe )) non modifica il filesystem. La pipe è anonima ; non ha nome nel filesystem . La shell usa la pipechiamata di sistema per crearla, no mkfifo. Utilizzare strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'per tracciare le chiamate di sistema di file e descrittori di file se si desidera vedere di persona. Su Linux, /dev/fd/63fa parte del /procfilesystem virtuale; ha automaticamente voci per ogni descrittore di file e non è una copia del contenuto. Quindi non puoi chiamarlo un "file temporaneo" a meno che non foo 3<bar.txtconti
Peter Cordes il

@PeterCordes Punti positivi. Ho incluso il tuo commento nella risposta per una maggiore visibilità.
VonC

1
@PeterCordes Lascerò qualsiasi modifica a te: questo è ciò che rende interessante Stack Overflow: chiunque può "risolvere" una risposta.
VonC

127

In bash è possibile utilizzare i subshells, per eseguire singolarmente le pipeline dei comandi, racchiudendo la pipeline tra parentesi. È quindi possibile aggiungere un prefisso a <per creare pipe denominate anonime che è quindi possibile passare a diff.

Per esempio:

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

Le pipe anonime nominate sono gestite da bash, quindi vengono create e distrutte automaticamente (a differenza dei file temporanei).


1
Molto più dettagliato della mia redazione sulla stessa soluzione - batch anonimo -. +1
VonC

4
Questo si chiama sostituzione del processo in Bash.
Franklin Yu,

5

Alcune persone che arrivano a questa pagina potrebbero essere alla ricerca di una diff linea per riga, per la quale commo invece grep -fdovrebbero essere utilizzate.

Una cosa da sottolineare è che, in tutti gli esempi di risposta, le differenze non inizieranno fino a quando entrambi i flussi non saranno terminati. Prova questo con es:

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

Se questo è un problema, potresti provare sd (stream diff), che non richiede l'ordinamento (come commfa) né elaborare la sostituzione come negli esempi precedenti, è ordini o grandezza più veloci di grep -f e supporta flussi infiniti.

L'esempio di test che propongo sarebbe scritto in questo modo in sd:

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

Ma la differenza è che seq 100sarebbe diversoseq 10 immediatamente. Si noti che, se uno dei flussi è a tail -f, il diff non può essere fatto con la sostituzione del processo.

Ecco un post sul blog che ho scritto su flussi diversi sul terminale, che introduce sd.

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.