Как отправить стандартный вывод одного процесса на несколько процессов с использованием (предпочтительно неназванных) труб в Unix (или Windows)?

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

         proc2 -> stdout
       /
 proc1
       \ 
         proc3 -> stdout

Я пробовал

 proc1 | (proc2 & proc3)

но он не работает, т.е.

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

пишет

 b23

в stdout вместо

 a23
 b23

Ответ 1

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

В 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

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

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

Ответ 2

Как сказал 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, но если это не так, заключите часть в обратные кавычки.

Ответ 3

Unix (bash, ksh, zsh)

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

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

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

Подводными камнями такого подхода являются:

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

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

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

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

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

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

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

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

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

    • Хотя zsh (похвально) по умолчанию синхронен с (гораздо более распространенными) заменами выходного процесса stdout, даже | cat Техника | 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 они вообще не работают.

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


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


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

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

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

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

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

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

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


Windows

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



bash -based, но в любом случае портативное решение Unix с синхронным выполнением и сериализацией вывода

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

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

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

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

Если вы сохраните приведенный ниже код как 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=( "[email protected]" )

# 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[@]}"

Ответ 4

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

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

a23
1b3

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

Ответ 5

другой способ сделать это будет,

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

выход:

a23
b23

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