Как отправить стандартный вывод одного процесса на несколько процессов с использованием (предпочтительно неназванных) труб в 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 b
echo 123 | tee >(tr 1 a) | tr 1 b
echo 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)
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
Короче говоря:
Если вы можете жить с этими ограничениями, использование замен процесса вывода является жизнеспособным вариантом (например, если все они записывают в отдельные файлы вывода).
Обратите внимание, что 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
нет необходимости создавать подоболочку здесь