Як я можу надіслати 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.