2008-12-05 12 views
110

Wie können Sie diff zwei Pipelines ohne temporäre Dateien in Bash? Angenommen, Sie zwei Befehlspipelines haben:Wie können Sie zwei Pipelines in Bash unterscheiden?

foo | bar 
baz | quux 

und Sie wollen die diff in ihre Ausgaben zu finden. Eine Lösung würde natürlich sein:

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

Ist es möglich, so ohne die Verwendung von temporären Dateien in Bash zu tun? Sie können in einen der Pipelines zu diff einer temporären Datei durch Rohrleitungen loszuwerden:

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

Aber Sie können nicht über die Pipeline beiden Pipelines in diff gleichzeitig (in keiner offensichtlichen Weise, zumindest). Gibt es einen cleveren Trick mit /dev/fd dies zu tun, ohne temporäre Dateien zu verwenden?

Antwort

112

Eine einzeilige mit 2 tmp-Dateien (nicht das, was Sie wollen) würde:

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

Mit bash, könnte man allerdings versuchen:

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

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

Die zweite Version wird mehr deutlich daran erinnern, welche Eingabe was war, indem
-- /dev/stdin vs ++ /dev/fd/63 oder etwas angezeigt wird, anstelle von zwei nummerierten fds.


Nicht einmal ein Named Pipe wird im Dateisystem erscheinen, zumindest auf OSes wo bash kann wie /dev/fd/63 mithilfe von Dateinamen Prozess Substitution implementieren, um einen Dateinamen zu erhalten, dass der Befehl öffnen und liest aus, um tatsächlich von einem lesen Bereits geöffneter Dateideskriptor, den bash vor dem Ausführen des Befehls eingerichtet hat. (Für diff auf fd 63. dh bash verwendet pipe(2) vor Gabel und dann dup2 von dem Ausgang der quux an einen Eingang Dateideskriptor zu umleiten)

ohne „magische“ /dev/fd oder /proc/self/fd Auf einem System schlag verwenden könnte benannte Pipes, um die Prozesssubstitution zu implementieren, aber im Gegensatz zu temporären Dateien würden sie sie zumindest selbst verwalten und Ihre Daten würden nicht in das Dateisystem geschrieben werden.

Sie können überprüfen, wie bash die Prozesssubstitution mit echo <(true) implementiert, um den Dateinamen zu drucken, anstatt von ihm zu lesen. Es druckt /dev/fd/63 auf einem typischen Linux-System. Oder für weitere Details über genau das, was Systemaufrufe bash verwendet, dieser Befehl auf einem Linux-System-Trace-Datei und Datei-Descriptor System ruft

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

Ohne bash, könnten Sie eine Named Pipe machen.Verwenden Sie -diff zu sagen, eine Eingabe von STDIN zu lesen, und verwenden Sie die Named Pipe als die anderen:

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

Hinweis, dass Sie nur Rohr einen Ausgang-mehr Eingänge mit dem Befehl tee :

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

der obige Befehl zeigt die Ausgabe des ls * .txt an das Terminal und gibt es in der Textdatei txtlist.txt.

Aber mit Prozess Substitution, können Sie tee verwenden die gleichen Daten in mehrere Pipelines zu füttern:

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

sogar ohne bash, können Sie temporäre fifo 's mfifo a verwenden; cmd> a & cmd2 | diff a -; rm a' – unhammer

+0

Sie können eine reguläre Pipe für einen der Argumente verwenden: 'pipeline1 | diff -u - <(pipeline2) '. Dann wird die Ausgabe deutlicher Sie daran erinnern, welche Eingabe war, indem Sie '_/dev/stdin' vs.' ++/dev/fd/67' oder etwas anstelle von zwei nummerierten fds zeigen. –

+0

Prozesssubstitution ('foo <(pipe)') ändert das Dateisystem nicht. ** Die Pfeife ist * anonym *; Es hat keinen Namen im Dateisystem **. Die Shell verwendet den Systemaufruf "pipe", um sie zu erstellen, nicht "mkfifo". Verwenden Sie 'strace -f -datei, desc, clone, execve bash -c '/ bin/true | diff -u - <(/ bin/true) ', um Datei- und Dateideskriptor-Systemaufrufe zu verfolgen, wenn Sie selbst sehen möchten.Unter Linux ist '/ dev/fd/63' Teil des'/proc' virtuellen Dateisystems; Es enthält automatisch Einträge für jeden Dateideskriptor, und es ist keine Kopie des Inhalts. Sie können das also nicht als "temporäre Datei" bezeichnen, es sei denn, 'foo 3

106

In bash Sie Subshells verwenden können, einzeln die Befehlsleitungen auszuführen, durch die Pipeline in Klammern einschließen. Sie können diese dann mit < voranstellen, um anonyme benannte Pipes zu erstellen, die Sie dann an diff übergeben können.

Zum Beispiel:

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

Die anonymen Named Pipes von bash verwaltet werden, so dass sie automatisch erstellt und zerstört werden (im Gegensatz zu temporären Dateien).

+1

Viel detaillierter als meine Redaktion auf der gleichen Lösung - anonyme Charge -. +1 – VonC

+4

Dies nennt man [process substitution] (https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html) in Bash. –

3

Einige Leute, die auf dieser Seite ankommen, suchen nach einem zeilenweisen Diff, für das stattdessen comm oder grep -f verwendet werden sollte.

Eine Sache zu betonen ist, dass die Diffs in allen Beispielen der Antwort erst starten, wenn beide Streams beendet sind. Testen Sie dies mit zB:

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

Wenn dies ein Problem ist, könnten Sie sd (Strom diff) versuchen, die Sortierung nicht erforderlich ist (wie comm der Fall ist), noch Verfahren Substitution wie die obigen Beispiele sind Größenordnung schneller als grep -f und unterstützt unendliche Ströme.

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

Aber der Unterschied ist, dass seq 100 mit seq 10 sofort diffed würde:

Der Test Beispiel, das ich vorschlagen würde, wie dies in sd geschrieben werden. Beachten Sie, dass, wenn einer der Streams ein tail -f ist, das Diff nicht mit Prozesssubstitution durchgeführt werden kann.

Hier ist ein blogpost Ich schrieb über Diffing-Streams auf dem Terminal, die sd eingeführt.