Как я могу отправить стандартный вывод одного процесса нескольким процессам, используя (желательно безымянные) каналы в Unix (или Windows)? - PullRequest
71 голосов
/ 14 сентября 2008

Я бы хотел перенаправить стандартный вывод процесса proc1 на два процесса proc2 и proc3:

         proc2 -> stdout
       /
 proc1
       \ 
         proc3 -> stdout

Я пытался

 proc1 | (proc2 & proc3)

но, похоже, не работает, т.е.

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

пишет

 b23

на стандартный вместо

 a23
 b23

Ответы [ 5 ]

120 голосов
/ 14 сентября 2008

Примечание редактора :
- >(…) - это замена процесса , которая является нестандартной функцией оболочки из некоторых POSIX-совместимых оболочек: bash, ksh, zsh.
- Этот ответ случайно отправляет выходные данные подстановки выходного процесса по конвейеру тоже : echo 123 | tee >(tr 1 a) | tr 1 b.
- Выходные данные из подстановок процесса будут непредсказуемо чередоваться, и, за исключением zsh, конвейер может завершиться до того, как команды внутри >(…) do.

В Unix (или на Mac) используйте команду tee :

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

Обычно вы используете tee для перенаправления вывода на несколько файлов, но с помощью> (...) вы можете перенаправить на другой процесс. Итак, в общем,

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

будет делать то, что вы хотите.

Под windows я не думаю, что встроенная оболочка имеет эквивалент. Microsoft Windows PowerShell имеет команду tee.

22 голосов
/ 10 октября 2008

Как сказал dF, bash позволяет использовать конструкцию >(…), запускающую команду вместо имени файла. (Существует также конструкция <(…), которая заменяет вывод другой команды вместо имени файла, но это не имеет значения, я упоминаю это только для полноты).

Если у вас нет bash или вы не работаете в системе с более старой версией bash, вы можете сделать вручную то, что делает bash, используя файлы FIFO.

Общий способ достичь того, что вы хотите, это:

  • определяет, сколько процессов должно получить выходные данные вашей команды, и создайте столько FIFO, предпочтительно в глобальной временной папке:
    subprocesses="a b c d"
    mypid=$$
    for i in $subprocesses # this way we are compatible with all sh-derived shells  
    do
        mkfifo /tmp/pipe.$mypid.$i
    done
  • запустить все ваши подпроцессы, ожидающие ввода от FIFO:
    for i in $subprocesses
    do
        tr 1 $i </tmp/pipe.$mypid.$i & # background!
    done
  • выполнить вашу команду на FIFO:
    proc1 | tee $(for i in $subprocesses; do echo /tmp/pipe.$mypid.$i; done)
  • наконец, удалите FIFO:
    for i in $subprocesses; do rm /tmp/pipe.$mypid.$i; done

ПРИМЕЧАНИЕ: по соображениям совместимости я бы сделал $(…) с обратными кавычками, но я не смог сделать это, написав этот ответ (обратная цитата используется в SO). Обычно $(…) достаточно стар, чтобы работать даже в старых версиях ksh, но если это не так, заключите часть в кавычки.

7 голосов
/ 11 мая 2017

Unix (bash, ksh, zsh)

ответ dF. содержит начальное значение ответа, основанного на tee и выводе подстановках процесса
(>(...)), что может или не может работать, в зависимости от ваших требований:

Обратите внимание, что технологические замены являются нестандартной функцией, которая (в основном) Оболочки только для POSIX-функций, такие как dash (который действует как /bin/sh в Ubuntu, например), поддержка не . Сценарии оболочки, ориентированные на /bin/sh, должны не полагаться на них.

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

Подводные камни этого подхода:

  • непредсказуемое асинхронное поведение вывода : выходные потоки от команд внутри замен процесса вывода >(...) чередуются непредсказуемым образом.

  • В bash и ksh (в отличие от zsh - но см. Исключение ниже):

    • вывод может поступить после выполнения команды.
    • последующие команды могут начать выполнение до того, как команды в подстановках процесса закончатся - bash и ksh do not ждать процесса вывода порожденные замещением процессы, по крайней мере, по умолчанию.
    • jmb хорошо описывает это в комментарии к ответу Д.Ф .:

имейте в виду, что команды, начатые внутри >(...), отделены от исходной оболочки, и вы не можете легко определить, когда они заканчиваются; tee завершит работу после записи всего, но замещенные процессы будут по-прежнему потреблять данные из различных буферов в ядре и файловом вводе-выводе, а также все время, затрачиваемое на их внутреннюю обработку данных. Вы можете столкнуться с условиями гонки, если ваша внешняя оболочка затем будет полагаться на все, что создается подпроцессами.

  • zsh является единственной оболочкой, которая по умолчанию ожидает завершения процессов, выполняемых в подстановках выходных процессов, , за исключением , если stderr перенаправлен на один (2> >(...)).

  • ksh (по крайней мере, начиная с версии 93u+) позволяет использовать без аргументов wait для ожидания завершения процессов, вызываемых подстановкой выходного процесса.
    Обратите внимание, что в интерактивном сеансе это может привести к ожиданию любых ожидающих фоновых заданий , однако.

  • bash v4.4+ может подождать самую последнюю запущенную замену процесса вывода с wait $!, но без аргументов wait не не работать, делая это непригодным для команды с множественными заменами выходных процессов.

  • Однако, bash и ksh могут быть вынуждены ждать , отправив команду на | cat, но учтите, что это заставляет команду работать в subshell . Предостережения

    • ksh (по состоянию на ksh 93u+) не поддерживает отправку stderr для замены процесса вывода (2> >(...)); такая попытка молча игнорируется .

    • Хотя zsh (похвально) синхронно по умолчанию с (гораздо более распространенными) stdout заменами выходного процесса, даже техника | cat не может сделать их синхронно с stderr заменами процесса вывода (2> >(...)).

  • Однако, , даже если вы гарантируете синхронное выполнение , проблема непредсказуемо чередуемого вывода остается.

Следующая команда, запущенная в bash или ksh, иллюстрирует проблемное поведение (вам может потребоваться выполнить ее несколько раз, чтобы увидеть оба признака): AFTER обычно выводит на печать перед выводится из выходных подстановок, и выходные данные из последних могут чередоваться непредсказуемо.

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

Короче :

  • Гарантирование конкретной выходной последовательности для каждой команды:

    • Ни bash, ни ksh, ни zsh не поддерживают это.
  • Синхронное исполнение:

    • Выполнимо, за исключением случаев с stderr -обработанными выходными процессами:
      • В zsh они неизменно асинхронны.
      • В ksh они вообще не работают .

Если вы можете жить с этими ограничениями, использование замен процесса вывода является жизнеспособным вариантом (например, если все они записывают в отдельные файлы вывода).


Обратите внимание, что гораздо более громоздкое, но потенциально POSIX-совместимое решение tzot также демонстрирует непредсказуемое поведение вывода ; однако, используя wait, вы можете гарантировать, что последующие команды не начнут выполняться, пока не завершатся все фоновые процессы.
См. Внизу для более надежная синхронная реализация с сериализованным выводом .


Единственное прямолинейное bash решение с предсказуемым поведением на выходе - это следующее, однако, чрезмерно медленное с большими входными наборами , потому что петли оболочки по своей природе медленные.
Также обратите внимание, что чередует строки вывода из целевых команд .

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

Unix (с использованием GNU Parallel)

Установка GNU parallel включает надежное решение с сериализованным (для каждой команды) выводом , что дополнительно позволяет параллельное выполнение :

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

parallel по умолчанию гарантирует, что выходные данные различных команд не чередуются (это поведение можно изменить - см. man parallel).

Примечание. Некоторые дистрибутивы Linux поставляются с другой утилитой parallel, которая не работает с приведенной выше командой; используйте parallel --version, чтобы определить, какой из них у вас есть.


Windows

Полезный ответ Джея Базузи показывает, как это сделать в PowerShell . Тем не менее, его ответ аналогичен зацикленному bash ответу выше, он будет непомерно медленным с большими входными наборами , а также чередует выходные строки из цели команды .



bash на основе, но в остальном портативное решение Unix с синхронным выполнением и сериализацией вывода

Ниже приводится простая, но достаточно надежная реализация подхода, представленного в ответе tzot , который дополнительно обеспечивает:

  • синхронное исполнение
  • сериализованный (сгруппированный) выход

Хотя он не является строго POSIX-совместимым, поскольку это сценарий bash, он должен быть переносимым на любую платформу Unix с bash.

Примечание. Более полную реализацию, выпущенную по лицензии MIT, можно найти в this Gist .

Если вы сохраните приведенный ниже код как скрипт fanout, сделаете его исполняемым и поместите int в свой PATH, команда из вопроса будет работать следующим образом:

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

fanout исходный код скрипта :

#!/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 голосов
/ 15 сентября 2008

Поскольку @dF: упомянул, что PowerShell имеет тройник, я подумал, что покажу способ сделать это в PowerShell.

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

a23
1b3

Обратите внимание, что каждый объект, выходящий из первой команды, обрабатывается до создания следующего объекта. Это может позволить масштабирование до очень больших входов.

0 голосов
/ 14 мая 2013

другим способом было бы,

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

выход:

a23
b23

здесь не нужно создавать подоболочку

...