Як я можу надіслати stdout одного процесу для кількох процесів, використовуючи (бажано неназвані) канали в Unix (або Windows)?


79

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

         proc2 -> stdout
       /
 proc1
       \ 
         proc3 -> stdout

я намагався

 proc1 | (proc2 & proc3)

але це, здається, не працює, тобто

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

пише

 b23

to stdout замість

 a23
 b23

Відповіді:


125

Примітка редактора :
- >(…)це процес заміщення , що є нестандартною особливістю оболонки з деяких POSIX-сумісних оболонок: bash, ksh, zsh.
- Ця відповідь випадково посилає вихід процесу заміщення вихідного через трубопровід занадто : 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 PowerShell від Microsoft має teeкоманду.


4
Це не конструкція POSIX і вимагає bash або ksh. Вам не пощастило з tcsh, dash тощо
pixelbeat

2
@pixelbeat: ... але його можна розбити на конструкції POSIX (див. мою відповідь :)
tzot

12
Це не робить точно того, що просив @secr. teeдодасть висновок перенаправлення процесу до, stdoutперш ніж відправляти його по каналу, що суттєво відрізняється від конвеєра того самого екземпляра stdoutдо кількох команд. Наприклад, @dF echo 123 | tee >(tr 1 a) | tr 2 bпризведе до 1b3 ab3, що не має сенсу в контексті вихідного запитання.
Dejay Clayton

8
Хоча це дуже зручно, майте на увазі, що команди, розпочаті всередині> (...), відокремлені від початкової оболонки, і ви не можете легко визначити, коли вони закінчуються; трійник закінчать після написання все, але заміщені процеси все одно будуть споживати дані з різних буферів в ядрі і файл вводу / виводу, а також незалежно від часу береться їх внутрішньої обробки даних. Ви можете зіткнутися з расовими умовами, якщо ваша зовнішня оболонка надалі покладається на все, що створюється підпроцесами.
jmb

3
@Dejay Clayton: Ви можете відкинути оригінальний вхід за допомогою inproc | tee >(outproc1) >(outproc2) > /dev/null | outproc. outproc бачитиме лише вихідні дані, вироблені outproc1 та outproc2. Оригінальне введення "зникло".
ack

22

Як сказав dF, bashдозволяє використовувати >(…)конструкцію, що виконує команду замість імені файлу. (Існує також <(…)конструкція, яка замінює висновок іншої команди замість імені файлу, але це зараз не має значення, я згадую це лише для повноти).

Якщо у вас немає bash або ви не працюєте в системі зі старішою версією bash, ви можете робити те, що робить bash, вручну, використовуючи файли FIFO.

Загальним способом досягнення бажаного є:

  • вирішіть, скільки процесів має отримувати результат вашої команди, і створіть стільки FIFO, бажано в глобальній тимчасовій папці:
    підпроцеси = "abc d"
    mypid = $$
    для i в $ підпроцесах # таким чином ми сумісні з усіма оболонками, похідними sh  
    робити
        mkfifo /tmp/pipe.$mypid.$i
    зроблено
  • запустити всі ваші підпроцеси, що очікують введення з FIFO:
    для i в $ підпроцесах
    робити
        tr 1 $ i </tmp/pipe.$mypid.$i & # background!
    зроблено
  • виконайте свою команду, потрапляючи в FIFO:
    proc1 | трійник $ (для i в $ підпроцесах; do echo /tmp/pipe.$mypid.$i; готово)
  • нарешті, видаліть файли FIFO:
    для i в $ підпроцесах; do rm /tmp/pipe.$mypid.$i; зроблено

ПРИМІТКА: з міркувань сумісності я б зробив це $(…)із зворотними цитатами, але не зміг зробити це, написавши цю відповідь (зворотне цитування використовується в SO). Зазвичай файл $(…)є достатньо старим, щоб працювати навіть у старих версіях ksh, але якщо ні, додайте частину в зворотні цитати.


1
++ для чудового підходу, але ви повинні mkfifoскоріше використовувати , ніж mknod, оскільки лише перший відповідає POSIX. Крім того, використання заміни команд без котирувань є крихким, і існує потенціал для використання глобалізації для підвищення ефективності. У своїй відповіді я взяв на себе свободу впровадження більш надійного рішення - хоч і bashзаснованого. Зверніть увагу, що це $(…)вже давно входить до складу POSIX, тому я б уникав менш передбачуваних `…`(і SO, безумовно, дозволяє використовувати `в кодових блоках і навіть вбудованих інтервалах коду (принаймні зараз :)).
mklement0

Здається, сторона запису заблокує, якщо один із процесів на стороні читання перестане споживати (тобто не вдасться запуститись, загине тощо). Щось, що слід враховувати, думаючи про необхідну стійкість вашого рішення.
Рассел Спейт

9

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нічого НЕ чекати , поки виходу процес заміщення-породжував процеси до кінця, по крайней мере , за замовчуванням.
    • jmb добре викладає це у коментарі до відповіді dF.:

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

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

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

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

  • Тим НЕ менше, bashі kshможе бути змушений чекати , перенаправивши команду | cat, але зауважте , що це робить команду запуску в субоболочке . Застереження :

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

    • Хоча за замовчуваннямzsh (похвально) синхронний із (набагато більш поширеними) підстановками вихідного процесу stdout , навіть техніка не може зробити їх синхронними із підстановками вихідного процесу stderr ( ).| cat2> >(...)

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

Наступна команда, коли запускається в 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 рішення також демонструє непередбачувану поведінку на виході ; однак, використовуючи waitви можете переконатися, що подальші команди не починають виконуватися, поки не закінчаться всі фонові процеси.
Див. Нижню частину для більш надійної, синхронної, серіалізованої реалізації .


Єдиним прямим bash рішенням із передбачуваною поведінкою на виході є наступне, що, однак, надмірно повільне при великих наборах введення , оскільки цикли оболонки за своєю суттю є повільними.
Також зауважте, що це чергує вихідні рядки цільових команд .

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

Unix (з використанням GNU Parallel)

Встановлення GNUparallel забезпечує надійне рішення із серіалізованим (на команду) виходом, що додатково дозволяє паралельне виконання :

$ 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, у цій статті .

Якщо ви збережете наведений нижче код як скрипт fanout, зробите його виконуваним і введете int your 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

Оскільки @dF: згадав, що PowerShell має трійник, я думав, що покажу спосіб зробити це в PowerShell.

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

a23
1b3

Зверніть увагу, що кожен об’єкт, що виходить з першої команди, обробляється до створення наступного об’єкта. Це може дозволити масштабування до дуже великих входів.


Так, але це рівноцінно виконанню while IFS= read -r line; do tr 1 a <<<"$line"; tr 1 b <<<"$line"; done < <(echo '123')в Bash, який добре масштабує пам'ять , але не продуктивність .
mklement0

1

Ви також можете зберегти вихідні дані у змінну та використовувати їх для інших процесів:

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

Однак це працює лише в тому випадку, якщо

  1. proc1 припиняється в якийсь момент :-)
  2. proc1 не дає занадто багато результату (не знаю, які обмеження існують, але це, мабуть, ваша оперативна пам’ять)

Але це легко запам’ятати і залишає вам більше можливостей щодо результатів, які ви отримуєте з процесів, які ви породили там, наприклад:

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

Мені було важко робити щось подібне із | tee >(proc2) >(proc3) >/dev/nullпідходом.


-1

інший спосіб зробити це,

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

вихід:

a23
b23

немає необхідності створювати тут під оболонку


На якій оболонці це працює? Все це вирівнює, echo 123 |{tr 1 a,tr 1 b}який скаржиться, що {trне існує, і якщо ви ставите зайві пробіли, він чекає додаткового введення через кому, а якщо ви заміните кому на крапку з комою або амперсанд, ви отримаєте лише перший надрукований - не обидва.
Джеррі Єремія

@JerryJeremiah: Він працює в оболонках , що в фігурних дужках ( bash, ksh, zsh) шляхом створення командного рядка echo '&& echo 123 |'{'tr 1 a','tr 1 b'} | sed -n 's/^&&//gp'в рядок , а потім передати цей рядок eval. Тим не менш, він (а) створює 3 під оболонки в процесі побудови рядка (1 для `...`, і 2 для сегментів вбудованого конвеєра, і (b), що важливіше, він дублює команду введення, щоб окрема копія виконується для кожної цільової trкоманди. Окрім неефективності, одна і та ж команда, що виконується двічі, не обов'язково дає однаковий результат двічі.
mklement0
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.