tee + cat: используйте вывод несколько раз и затем объединяйте результаты

Если я вызываю некоторую команду, например, echo я могу использовать результаты этой команды в нескольких других командах с tee . Пример:

 echo "Hello world!" | tee >(command1) >(command2) >(command3) 

С cat я могу собрать результаты нескольких команд. Пример:

 cat <(command1) <(command2) <(command3) 

Я хотел бы иметь возможность делать обе вещи одновременно, чтобы я мог использовать tee для вызова этих команд на выходе чего-то еще (например, echo я написал), а затем собрать все их результаты на одном выход с cat .

Важно сохранить результаты в порядке, это означает, что строки в command3 и command3 не должны быть переплетены, а упорядочены как команды (как это происходит с cat ).

Могут быть лучшие варианты, чем cat и tee но те, которые я знаю до сих пор.

Я хочу избежать использования временных файлов, потому что размер ввода и вывода может быть большим.

Как я могу это сделать?

PD: еще одна проблема заключается в том, что это происходит в цикле, что затрудняет обработку временных файлов. Это текущий код, который у меня есть, и он работает для небольших тестовых систем, но он создает бесконечные циклы при чтении и записи из auxfile, каким-то образом я не понимаю.

 somefunction() { if [ $1 -eq 1 ] then echo "Hello world!" else somefunction $(( $1 - 1 )) > auxfile cat <(command1 < auxfile) \ <(command2 < auxfile) \ <(command3 < auxfile) fi } 

Чтения и записи в auxfile кажутся перекрывающимися, заставляя все взорваться.

3 Solutions collect form web for “tee + cat: используйте вывод несколько раз и затем объединяйте результаты”

Вы можете использовать комбинацию GNU stdbuf и pee from moreutils :

 echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output 

pee popen(3) s эти 3 командные строки оболочки, а затем fread s вход и fwrite s все три, которые будут буферизированы до 1M.

Идея состоит в том, чтобы иметь буфер, по крайней мере такой же большой, как и вход. Таким образом, несмотря на то, что три команды запускаются одновременно, они будут видеть только входные данные, когда pee pclose s три команды последовательно.

На каждом pclose pee сбрасывает буфер в команду и ждет его завершения. Это гарантирует, что до тех cmdx команды cmdx не начнут выводить что-либо, прежде чем они cmdx какой-либо вход (и не разветвят процесс, который может продолжить вывод после возвращения родителя), вывод трех команд не будет быть чередующимся.

По сути, это немного похоже на использование временного файла в памяти, с недостатком, что три команды запускаются одновременно.

Чтобы избежать одновременного запуска команд, вы можете написать pee как функцию оболочки:

 pee() ( input=$(cat; echo .) for i do printf %s "${input%.}" | eval "$i" done ) echo "Hello world!" | pee cmd1 cmd2 cmd3 > out 

Но будьте осторожны, чтобы оболочки, отличные от zsh не сработали для двоичного ввода с символами NUL.

Это позволяет избежать использования временных файлов, но это означает, что весь ввод хранится в памяти.

В любом случае вам нужно будет хранить вход где-нибудь, в памяти или временном файле.

На самом деле, это довольно интересный вопрос, поскольку он показывает нам предел идеи Unix о том, что несколько простых инструментов взаимодействуют с одной задачей.

Здесь мы хотели бы иметь несколько инструментов, взаимодействующих с задачей:

  • команда источника (здесь echo )
  • диспетчерская команда ( tee )
  • некоторые команды фильтра ( cmd1 , cmd2 , cmd3 )
  • и команду агрегации ( cat ).

Было бы неплохо, если бы они могли одновременно работать вместе и выполнять свою тяжелую работу над данными, которые они должны обрабатывать, как только они будут доступны.

В случае одной команды фильтра это легко:

 src | tee | cmd1 | cat 

Все команды запускаются одновременно, cmd1 начинает cmd1 данные из src как только он будет доступен.

Теперь, с тремя командами фильтра, мы все равно можем сделать то же самое: запустите их одновременно и соедините их с трубами:

  ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓ ┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃ ┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃ ┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓ ┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃ ┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛ ┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃ ┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃ ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛ 

Что мы можем сделать относительно легко с именованными каналами :

 pee() ( mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat { tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0 eval "$1 < tee-cmd1 1<> cmd1-cat &" eval "$2 < tee-cmd2 1<> cmd2-cat &" eval "$3 < tee-cmd3 1<> cmd3-cat &" exec cat cmd1-cat cmd2-cat cmd3-cat ) echo abc | pee 'tr a A' 'tr b B' 'tr c C' 

(выше } 3<&0 состоит в том, чтобы обойти тот факт, что & перенаправляет stdin из /dev/null , и мы используем <> чтобы избежать открытия блокировок до тех пор, пока не откроется другой конец ( cat )).

Или, чтобы избежать названных каналов, немного больнее с zsh coproc:

 pee() ( n=0 ci= co= is=() os=() for cmd do eval "coproc $cmd $ci $co" exec {i}<&p {o}>&p is+=($i) os+=($o) eval i$n=$io$n=$o ci+=" {i$n}<&-" co+=" {o$n}>&-" ((n++)) done coproc : read -p eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co ) echo abc | pee 'tr a A' 'tr b B' 'tr c C' 

Теперь возникает вопрос: как только все программы будут запущены и подключены, будет ли поток данных?

У нас есть два противопоказания:

  • tee передает все свои выходы с одинаковой скоростью, поэтому он может отправлять данные только по скорости своего самого медленного выходного канала.
  • cat начнет считывать только со второй трубы (труба 6 на рисунке выше), когда все данные будут считаны с первого (5).

Это означает, что данные не будут течь в трубе 6 до cmd1 . И, как и в случае tr b B выше, это может означать, что данные также не будут поступать в трубу 3, что означает, что он не будет течь ни в одной из труб 2, 3 или 4, поскольку tee питается с наименьшей скоростью всех 3.

На практике эти трубы имеют нулевой размер, поэтому некоторым данным удастся пройти, и по крайней мере в моей системе я могу заставить его работать:

 yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c 

Помимо этого, с

 yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c 

У нас тупик, где мы находимся в такой ситуации:

  ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓ ┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃ ┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃ ┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓ ┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃ ┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛ ┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃ ┃ ┃██████████┃cmd3┃██████████┃ ┃ ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛ 

Мы заполнили трубы 3 и 6 (по 64кБ каждый). tee прочитал этот дополнительный байт, он cmd1 его на cmd1 , но

  • теперь он заблокирован, записывая на трубу 3, поскольку он ждет cmd2 чтобы его cmd2
  • cmd2 не может опорожнить его, потому что он заблокирован, записывая на трубу 6, ожидая, пока cat ее cmd2
  • cat не может его опорожнить, потому что он ждет, пока на трубе 5 больше не будет входа.
  • cmd1 не может сказать, что у cat нет больше входных данных, потому что он ждет больше ввода от tee .
  • и tee не может сказать cmd1 , нет больше ввода, потому что он заблокирован … и так далее.

У нас есть цикл зависимостей и, таким образом, тупик.

Теперь, каково решение? Большие трубы 3 и 4 (достаточно большие, чтобы содержать весь выход src ) будут делать это. Мы могли бы сделать это, например, вставив pv -qB 1G между tee и cmd2/3 где pv может хранить до 1G данных, ожидающих cmd2 и cmd3 чтобы их прочитать. Это означало бы две вещи:

  1. это использует потенциально много памяти и, более того, дублирует его
  2. это не cmd2 что все 3 команды будут взаимодействовать, потому что cmd2 на самом деле начнет обрабатывать данные только после завершения cmd1.

Решение второй проблемы заключалось бы в том, чтобы сделать трубы 6 и 7 больше. Предполагая, что cmd2 и cmd3 производят столько вывода, сколько они потребляют, это не будет потреблять больше памяти.

Единственный способ избежать дублирования данных (в первой задаче) – реализовать сохранение данных в самом диспетчере, то есть реализовать вариацию на tee которая может подавать данные со скоростью самого быстрого выхода (сохранение данных для подачи тем медленнее в своем темпе). Не совсем тривиально.

Итак, в конце концов, лучшее, что мы можем получить без программирования, вероятно, что-то вроде (синтаксис Zsh):

 max_hold=1G pee() ( n=0 ci= co= is=() os=() for cmd do if ((n)); then eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co" else eval "coproc $cmd $ci $co" fi exec {i}<&p {o}>&p is+=($i) os+=($o) eval i$n=$io$n=$o ci+=" {i$n}<&-" co+=" {o$n}>&-" ((n++)) done coproc : read -p eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co ) yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c 

То, что вы предлагаете, не может быть легко выполнено с помощью любой существующей команды и в любом случае не имеет большого смысла. Вся идея труб ( | в Unix / Linux) заключается в том, что в cmd1 | cmd2 cmd1 | cmd2 cmd1 записывает выходные данные (не более), пока буфер памяти не заполнит, а затем cmd2 считывать данные из буфера (не более) до тех пор, пока он не станет пустым. cmd1 есть, cmd1 и cmd2 работают одновременно, никогда не требуется иметь более чем ограниченный объем данных «в полете» между ними. Если вы хотите подключить несколько входов к одному выходу, если один из читателей отстает от остальных, то вы остановите других (какова точка запуска параллельно?) Или вы сбрасываете вывод, который лаггер еще не прочитал (в чем смысл не иметь промежуточный файл?). Плюс вся синхронизация становится намного сложнее.

В течение почти 30 лет опыта работы с Unix я не помню никакой ситуации, которая действительно принесла бы пользу для такого многоканального выхода.

Вы можете объединить несколько выходов в один поток сегодня, просто не cmd1 чередованием (как должны быть чередованием выходы cmd1 и cmd2 , одна очередь в очереди? По очереди записывает 10 байтов чередующихся «абзацев», как-то определенных, и если один не пишет ничего в течение длительного времени, все это сложно справиться). Это делается, например, (cmd1; cmd2; cmd3) | cmd4 (cmd1; cmd2; cmd3) | cmd4 , программы cmd1 , cmd2 и cmd3 запускаются один за другим, вывод отправляется как вход в cmd4 .

Для вашей проблемы перекрытия, в Linux (и с bash или zsh но не с ksh93 ), вы можете сделать это как:

 somefunction() ( if [ "$1" -eq 1 ] then echo "Hello world!" else exec 3> auxfile rm -f auxfile somefunction "$(($1 - 1))" >&3 auxfile 3>&- exec cat <(command1 < /dev/fd/3) \ <(command2 < /dev/fd/3) \ <(command3 < /dev/fd/3) fi ) 

Обратите внимание на использование (...) вместо {...} чтобы получить новый процесс на каждой итерации, чтобы мы могли иметь новый fd 3, указывающий на новый auxfile . < /dev/fd/3 – это трюк для доступа к этому удаленному файлу. Он не будет работать в системах, отличных от Linux, где < /dev/fd/3 подобен dup2(3, 0) и поэтому fd 0 будет открыт в режиме только для записи с курсором в конце файла.

Чтобы избежать вилки для вложенной функции, вы можете написать ее как:

 somefunction() { if [ "$1" -eq 1 ] then echo "Hello world!" else { rm -f auxfile somefunction "$(($1 - 1))" >&3 auxfile 3>&- exec cat <(command1 < /dev/fd/3) \ <(command2 < /dev/fd/3) \ <(command3 < /dev/fd/3) } 3> auxfile fi } 

Оболочка будет заботиться о резервном копировании fd 3 на каждой итерации. Однако в конечном итоге у вас закончится файловый дескриптор.

Хотя вы обнаружите, что более эффективно это делать как:

 somefunction() { if [ "$1" -eq 1 ]; then echo "Hello world!" > auxfile else somefunction "$(($1 - 1))" { rm -f auxfile cat <(command1 < /dev/fd/3) \ <(command2 < /dev/fd/3) \ <(command3 < /dev/fd/3) > auxfile } 3< auxfile fi } somefunction 12; cat auxfile 

То есть, не вставляйте перенаправления.

Interesting Posts

Некоторые пакеты в мяте не обновляются

помощь, необходимая для расшифровки этого ACPI-кода Acer Aspire 4830TG для Nvidia Optimus

Невозможно получить rEFIt для загрузки после установки (Ubuntu + OSX на macbook pro)

Xmonad: плавайте и изменяйте размеры окон до «натурального размера»

Подключение NFS не будет установлено в Dolphin

Таблица разделов, не распознанная ядром Linux

= sign в конце .sock-файлов в выводе ls

Как определить количество строк и столбцов в окне gnome-terminal при изменении размера?

Как добавить пусковую установку, которая компилирует некоторый код в терминале в XFce?

Как я могу распаковать gz-файлы в произвольных каталогах и хранить распакованные файлы в тех же самых каталогах?

Как получить журнал из u-boot без ком-порта

unbuffer / expect проглатывает rcode, когда его дочерний сигнал сигнализируется

Как запросить подпись EFI

Есть ли комбинация клавиш, чтобы окно занимало половину экрана?

Сделать запуск программы на втором виртуальном рабочем столе (XFCE)

Linux и Unix - лучшая ОС в мире.