Wie kann ich die Standardausgabe eines Prozesses mithilfe von (vorzugsweise unbenannten) Pipes unter Unix (oder Windows) an mehrere Prozesse senden?


78

Ich möchte die Standardausgabe von Prozess proc1 auf zwei Prozesse proc2 und proc3 umleiten:

         proc2 -> stdout
       /
 proc1
       \ 
         proc3 -> stdout

Ich habe es versucht

 proc1 | (proc2 & proc3)

aber es scheint nicht zu funktionieren, dh

 echo 123 | (tr 1 a & tr 1 b)

schreibt

 b23

statt statt zu stdout

 a23
 b23

Antworten:


124

Anmerkung der Redaktion :
- >(…)ist ein Prozess Substitution , die eine ist Nicht - Standard - Shell - Funktion von einigem POSIX-kompatible Shells: bash, ksh, zsh.
- Diese Antwort sendet versehentlich den Ausgabeprozess Auswechselung Ausgang durch die Pipeline zu : echo 123 | tee >(tr 1 a) | tr 1 b.
- Die Ausgabe der Prozessersetzungen wird unvorhersehbar verschachtelt, und außer in zshkann die Pipeline beendet werden, bevor die darin enthaltenen Befehle ausgeführt werden >(…).

Verwenden Sie unter Unix (oder auf einem Mac) den folgenden teeBefehl :

$ echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
b23
a23

Normalerweise würden Sie verwenden tee, um die Ausgabe in mehrere Dateien umzuleiten, aber mit> (...) können Sie zu einem anderen Prozess umleiten. Also im Allgemeinen

$ proc1 | tee >(proc2) ... >(procN-1) >(procN) >/dev/null

wird tun was du willst.

Unter Fenstern glaube ich nicht, dass die eingebaute Shell ein Äquivalent hat. Die Windows PowerShell von Microsoft verfügt jedoch über einen teeBefehl.


4
Dies ist kein POSIX-Konstrukt und erfordert bash oder ksh. Du hast
kein

2
@pixelbeat:… aber es kann in POSIX-Konstrukte zerlegt werden (siehe meine Antwort :)
tzot

12
Dies macht nicht genau das, was @secr angefordert hat. teeHängt die Ausgabe einer Prozessumleitung an, stdoutbevor sie über die Pipe gesendet wird. Dies unterscheidet sich deutlich von der Weiterleitung derselben Instanz stdoutan mehrere Befehle. @dF führt beispielsweise echo 123 | tee >(tr 1 a) | tr 2 bdazu 1b3 ab3, was im Kontext der ursprünglichen Frage keinen Sinn ergibt .
Dejay Clayton

8
Beachten Sie, dass die in> (...) gestarteten Befehle von der ursprünglichen Shell getrennt sind und Sie nicht leicht feststellen können, wann sie beendet werden. Das T-Stück wird beendet, nachdem alles geschrieben wurde, aber die ersetzten Prozesse verbrauchen weiterhin die Daten aus verschiedenen Puffern im Kernel und in der Datei-E / A sowie die Zeit, die für die interne Verarbeitung der Daten benötigt wird. Sie können auf Rennbedingungen stoßen, wenn Ihre äußere Hülle sich dann auf alles verlässt, was durch die Unterprozesse erzeugt wird.
JMB

2
@Dejay Clayton: Sie können die ursprüngliche Eingabe mit verwerfen inproc | tee >(outproc1) >(outproc2) > /dev/null | outproc. outproc sieht nur die von outproc1 und outproc2 erzeugte Ausgabe. Die ursprüngliche Eingabe ist "weg".
Bestätigen Sie den

22

Wie dF sagte, basherlaubt es, das >(…)Konstrukt zu verwenden, das einen Befehl anstelle eines Dateinamens ausführt. (Es gibt auch das <(…)Konstrukt, das die Ausgabe eines anderen Befehls anstelle eines Dateinamens ersetzt, aber das ist jetzt irrelevant, ich erwähne es nur der Vollständigkeit halber).

Wenn Sie kein Bash haben oder auf einem System mit einer älteren Version von Bash ausgeführt werden, können Sie manuell das tun, was Bash tut, indem Sie FIFO-Dateien verwenden.

Der generische Weg, um das zu erreichen, was Sie wollen, ist:

  • Entscheiden Sie, wie viele Prozesse die Ausgabe Ihres Befehls empfangen sollen, und erstellen Sie so viele FIFOs, vorzugsweise in einem globalen temporären Ordner:
    Unterprozesse = "abc d"
    mypid = $$
    Für i in $ subprocesses # sind wir auf diese Weise mit allen von sh abgeleiteten Shells kompatibel  
    tun
        mkfifo /tmp/pipe.$mypid.$i
    getan
  • Starten Sie alle Ihre Unterprozesse, die auf die Eingabe von den FIFOs warten:
    für i in $ Unterprozessen
    tun
        tr 1 $ i </tmp/pipe.$mypid.$i & # Hintergrund!
    getan
  • Führen Sie Ihren Befehlsabschlag zu den FIFOs aus:
    proc1 | tee $ (für i in $ subprocesses; echo /tmp/pipe.$mypid.$i; done)
  • Entfernen Sie zum Schluss die FIFOs:
    für i in $ Unterprozessen; do rm /tmp/pipe.$mypid.$i; getan

HINWEIS: Aus Kompatibilitätsgründen würde ich dies $(…)mit Backquotes tun , aber ich konnte diese Antwort nicht schreiben (das Backquote wird in SO verwendet). Normalerweise ist das $(…)alt genug, um auch in alten Versionen von ksh zu funktionieren. Wenn dies nicht der Fall ist, fügen Sie den Teil in Anführungszeichen ein.


1
++ für einen großartigen Ansatz, den Sie jedoch mkfifoeher als verwenden sollten mknod, da nur der erstere POSIX-kompatibel ist. Außerdem ist die Verwendung von nicht zitierten Befehlsersetzungen spröde, und es besteht die Möglichkeit, Globbing aus Effizienzgründen zu verwenden. Ich habe bashmir erlaubt, in meiner Antwort eine robustere - wenn auch basierte - Lösung zu implementieren . Beachten Sie, dass dies $(…)schon lange Teil von POSIX ist, daher würde ich mich von weniger vorhersehbaren fernhalten `…`(und SO erlaubt definitiv die Verwendung `in Codeblöcken und sogar Inline-Code-Bereichen (zumindest jetzt :)).
mklement0

Es scheint, dass die Schreibseite blockiert wird, wenn einer der Prozesse auf der Leseseite nicht mehr verbraucht wird (dh nicht gestartet werden kann, stirbt usw.). Beachten Sie dies, wenn Sie über die erforderliche Ausfallsicherheit Ihrer Lösung nachdenken.
Russell Speight

9

Unix ( bash, ksh, zsh)

Die Antwort von dF. enthält den Keim einer Antwort, die auf Substitutionen des Prozessprozesses basiert teeund diese ausgibt ( ) , die je nach Ihren Anforderungen möglicherweise funktionieren oder nicht :
>(...)

Beachten Sie, dass Prozessersetzungen eine nicht standardmäßige Funktion sind, die (meistens) nur POSIX-Features-Shells wie dash(die beispielsweise /bin/shunter Ubuntu funktionieren) nicht unterstützen. Das Targeting von Shell-Skripten /bin/shsollte sich nicht auf diese verlassen.

echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null

Die Fallstricke dieses Ansatzes sind:

  • unvorhersehbares, asynchrones Ausgabeverhalten : Die Ausgabestreams der Befehle innerhalb der Ausgabeprozessersetzungen >(...)verschachteln sich auf unvorhersehbare Weise.

  • In bashund ksh(im Gegensatz zu zsh- aber siehe Ausnahme unten):

    • Die Ausgabe kann nach Beendigung des Befehls eintreffen .
    • nachfolgende Befehle können die Ausführung beginnen , bevor die Befehle in der Prozess Ersetzungen beendet haben - bashund kshSie nicht warten , bis der Ausgabeprozess substitutions gelaicht Prozesse zu beenden, zumindest in der Standardeinstellung.
    • jmb bringt es gut in einen Kommentar zur Antwort von dF.:

Beachten Sie, dass die darin gestarteten Befehle >(...)von der ursprünglichen Shell getrennt sind und Sie nicht leicht feststellen können, wann sie beendet sind. Das teewird beendet, nachdem alles geschrieben wurde, aber die ersetzten Prozesse verbrauchen immer noch die Daten aus verschiedenen Puffern im Kernel und in der Datei-E / A sowie die Zeit, die für die interne Verarbeitung der Daten benötigt wird. Sie können auf Rennbedingungen stoßen, wenn Ihre äußere Hülle sich dann auf alles verlässt, was durch die Unterprozesse erzeugt wird.

  • zshist die einzige Shell, die standardmäßig auf den Abschluss der in den Ausgabeprozess-Ersetzungen ausgeführten Prozesse wartet , es sei denn, es handelt sich um stderr , das zu one ( 2> >(...)) umgeleitet wird .

  • ksh(zumindest ab Version 93u+) ermöglicht die Verwendung von argumentlosen wait, um auf den Abschluss der durch Substitution hervorgerufenen Prozesse des Ausgabeprozesses zu warten.
    Beachten Sie jedoch, dass in einer interaktiven Sitzung möglicherweise auch auf ausstehende Hintergrundjobs gewartet wird .

  • bash v4.4+Ich kann auf die zuletzt gestartete Ersetzung des Ausgabeprozesses mit warten wait $!, aber ohne Argument waitfunktioniert dies nicht. Dies ist für einen Befehl mit mehreren Ersetzungen des Ausgabeprozesses ungeeignet .

  • Jedoch bashund kshkann gezwungen zu warten , durch Rohrleitungen , den Befehl | cat, aber beachten Sie, dass dies macht den Befehl ausführen in einer Sub - Shell . Vorsichtsmaßnahmen :

    • ksh(ab ksh 93u+) unterstützt das Senden von stderr an eine Ausgabeprozessersetzung nicht ( 2> >(...)); Ein solcher Versuch wird stillschweigend ignoriert .

    • Während zshes (lobenswerterweise) standardmäßig mit den (weitaus häufigeren) stdout- Ausgabeprozess-Ersetzungen | catsynchron ist , kann selbst die Technik sie nicht mit stderr- Ausgabeprozess-Ersetzungen synchronisieren ( 2> >(...)).

  • Aber auch wenn Sie sicherstellen , synchrone Ausführung , das Problem der unvorhersehbar verschachtelten Ausgang bleibt.

Der folgende Befehl veranschaulicht beim Ausführen in bashoder kshdas problematische Verhalten (möglicherweise müssen Sie ihn mehrmals ausführen, um beide Symptome zu sehen ): Der Befehl AFTERwird normalerweise vor der Ausgabe der Ausgabesubstitutionen gedruckt , und die Ausgabe der letzteren kann unvorhersehbar verschachtelt werden.

printf 'line %s\n' {1..30} | tee >(cat -n) >(cat -n) >/dev/null; echo AFTER

Kurzum :

  • Garantieren einer bestimmten Ausgabesequenz pro Befehl:

    • Weder bashnoch kshnoch zshunterstützen wir das.
  • Synchrone Ausführung:

    • Machbar, außer bei stderr- bezogenen Ausgabeprozess-Ersetzungen:
      • In zshsind sie immer asynchron.
      • In arbeiten kshsie überhaupt nicht .

Wenn Sie mit diesen Einschränkungen leben können, ist die Verwendung von Ausgabeprozessersetzungen eine praktikable Option (z. B. wenn alle in separate Ausgabedateien schreiben).


Beachten Sie, dass die viel umständlichere, aber möglicherweise POSIX-kompatible Lösung von tzot auch ein unvorhersehbares Ausgabeverhalten aufweist . Durch die Verwendung können waitSie jedoch sicherstellen, dass nachfolgende Befehle erst ausgeführt werden, wenn alle Hintergrundprozesse abgeschlossen sind.
Siehe unten für eine robustere, synchron, serialisiert-Ausgang Implementierung .


Die einzige einfache bash Lösung mit vorhersehbarem Ausgabeverhalten ist die folgende, die jedoch bei großen Eingabesätzen unerschwinglich langsam ist , da Shell-Schleifen von Natur aus langsam sind.
Beachten Sie auch, dass dies die Ausgabezeilen von den Zielbefehlen abwechselt .

while IFS= read -r line; do 
  tr 1 a <<<"$line"
  tr 1 b <<<"$line"
done < <(echo '123')

Unix (mit GNU Parallel)

Die Installation von GNUparallel ermöglicht eine robuste Lösung mit serialisierter Ausgabe (pro Befehl) , die zusätzlich eine parallele Ausführung ermöglicht :

$ echo '123' | parallel --pipe --tee {} ::: 'tr 1 a' 'tr 1 b'
a23
b23

parallelStandardmäßig wird sichergestellt, dass die Ausgabe der verschiedenen Befehle nicht verschachtelt wird (dieses Verhalten kann geändert werden - siehe man parallel).

Hinweis: Einige Linux-Distributionen verfügen über ein anderes parallel Dienstprogramm, das mit dem obigen Befehl nicht funktioniert. Verwenden Sie parallel --versiondiese Option, um festzustellen, welche Sie gegebenenfalls haben.


Windows

Die hilfreiche Antwort von Jay Bazuzi zeigt, wie es in PowerShell geht . Dass sagte: seine Antwort ist die analoge die Looping bashAntwort oben, es wird untragbar langsam mit großen Eingangsmengen und auch wechselt die Ausgangsleitungen von den Zielbefehlen .



bash-basierte, aber ansonsten portable Unix-Lösung mit synchroner Ausführung und Ausgabeserialisierung

Das Folgende ist eine einfache, aber einigermaßen robuste Implementierung des in der Antwort von tzot vorgestellten Ansatzes , der zusätzlich Folgendes vorsieht:

  • synchrone Ausführung
  • serialisierte (gruppierte) Ausgabe

Obwohl es nicht streng POSIX-konform ist, da es sich um ein bashSkript handelt, sollte es auf jede Unix-Plattform portierbar seinbash .

Hinweis: In dieser Übersicht finden Sie eine umfassendere Implementierung, die unter der MIT-Lizenz veröffentlicht wurde .

Wenn Sie den folgenden Code als Skript speichern fanout, ihn ausführbar machen und in Ihren Code einfügen , PATHfunktioniert der Befehl aus der Frage wie folgt:

$ echo 123 | fanout 'tr 1 a' 'tr 1 b'
# tr 1 a
a23
# tr 1 b
b23

fanoutSkript-Quellcode :

#!/usr/bin/env bash

# The commands to pipe to, passed as a single string each.
aCmds=( "$@" )

# Create a temp. directory to hold all FIFOs and captured output.
tmpDir="${TMPDIR:-/tmp}/$kTHIS_NAME-$$-$(date +%s)-$RANDOM"
mkdir "$tmpDir" || exit
# Set up a trap that automatically removes the temp dir. when this script
# exits.
trap 'rm -rf "$tmpDir"' EXIT 

# Determine the number padding for the sequential FIFO / output-capture names, 
# so that *alphabetic* sorting, as done by *globbing* is equivalent to
# *numerical* sorting.
maxNdx=$(( $# - 1 ))
fmtString="%0${#maxNdx}d"

# Create the FIFO and output-capture filename arrays
aFifos=() aOutFiles=()
for (( i = 0; i <= maxNdx; ++i )); do
  printf -v suffix "$fmtString" $i
  aFifos[i]="$tmpDir/fifo-$suffix"
  aOutFiles[i]="$tmpDir/out-$suffix"
done

# Create the FIFOs.
mkfifo "${aFifos[@]}" || exit

# Start all commands in the background, each reading from a dedicated FIFO.
for (( i = 0; i <= maxNdx; ++i )); do
  fifo=${aFifos[i]}
  outFile=${aOutFiles[i]}
  cmd=${aCmds[i]}
  printf '# %s\n' "$cmd" > "$outFile"
  eval "$cmd" < "$fifo" >> "$outFile" &
done

# Now tee stdin to all FIFOs.
tee "${aFifos[@]}" >/dev/null || exit

# Wait for all background processes to finish.
wait

# Print all captured stdout output, grouped by target command, in sequences.
cat "${aOutFiles[@]}"

5

Da @dF: erwähnt hat, dass PowerShell ein Tee hat, dachte ich, ich würde einen Weg zeigen, dies in PowerShell zu tun.

PS > "123" | % { 
    $_.Replace( "1", "a"), 
    $_.Replace( "2", "b" ) 
}

a23
1b3

Beachten Sie, dass jedes Objekt, das aus dem ersten Befehl kommt, verarbeitet wird, bevor das nächste Objekt erstellt wird. Dies kann eine Skalierung auf sehr große Eingaben ermöglichen.


Ja, aber dies ist das Äquivalent zu while IFS= read -r line; do tr 1 a <<<"$line"; tr 1 b <<<"$line"; done < <(echo '123')Bash, das in Bezug auf das Gedächtnis gut skaliert , aber nicht in Bezug auf die Leistung .
mklement0

1

Sie können die Ausgabe auch in einer Variablen speichern und für die anderen Prozesse verwenden:

out=$(proc1); echo "$out" | proc2; echo "$out" | proc3

Dies funktioniert jedoch nur, wenn

  1. proc1 endet irgendwann :-)
  2. proc1 produziert nicht zu viel Ausgabe (weiß nicht, wo die Grenzen liegen, aber es ist wahrscheinlich dein RAM)

Es ist jedoch leicht zu merken und bietet Ihnen mehr Optionen für die Ausgabe der Prozesse, die Sie dort erzeugt haben, z.

out=$(proc1); echo $(echo "$out" | proc2) / $(echo "$out" | proc3) | bc

Ich hatte Schwierigkeiten, so etwas mit dem | tee >(proc2) >(proc3) >/dev/nullAnsatz zu machen.


-1

Ein anderer Weg wäre:

 eval `echo '&& echo 123 |'{'tr 1 a','tr 1 b'} | sed -n 's/^&&//gp'`

Ausgabe:

a23
b23

Hier muss keine Unterschale erstellt werden


Auf welcher Shell funktioniert das? Alles, was es tut, wird ausgewertet, echo 123 |{tr 1 a,tr 1 b}was sich beschwert, dass {tres nicht existiert. Wenn Sie zusätzliche Leerzeichen eingeben, wartet es aufgrund des Kommas auf zusätzliche Eingaben. Wenn Sie das Komma in ein Semikolon oder ein kaufmännisches Und ändern, wird nur das erste gedruckt - nicht beide.
Jerry Jeremiah

@JerryJeremiah: Es arbeitet in Schalen , die Verstrebung Expansion ( bash, ksh, zsh) durch die Befehlszeile zu schaffen echo '&& echo 123 |'{'tr 1 a','tr 1 b'} | sed -n 's/^&&//gp'in einer Zeichenfolge und dann diese Zeichenfolge vorbei eval. Das heißt, es (a) erstellt 3 Unterschalen beim Erstellen der Zeichenfolge (1 für die `...`und 2 für die Segmente der eingebetteten Pipeline, und (b), was noch wichtiger ist, dupliziert den Eingabebefehl, so dass eine separate Kopie erstellt wird wird für jeden Zielbefehl trausgeführt. Abgesehen von der Ineffizienz führt der zweimalige Ausführen desselben Befehls nicht unbedingt zweimal zur gleichen Ausgabe.
mklement0
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.