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

18

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

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

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

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

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

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

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

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

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

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

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

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

Trylks
источник
2
Какого размера мы говорим? Ваши требования заставляют все храниться в памяти. Сохранение результатов по порядку означает, что команда 1 должна завершиться первой (так что она предположительно прочитала весь ввод и напечатала весь вывод), прежде чем команды 2 и команда 3 могут даже начать обработку (если вы сначала тоже не захотите собрать их вывод в памяти).
frostschutz
вы правы, ввод и вывод command2 и command3 слишком велики для хранения в памяти. Я ожидал, что использование свопа будет работать лучше, чем использование временных файлов. Другая проблема, которая у меня есть, заключается в том, что это происходит в цикле, и это делает обработку файлов еще сложнее. Я использую один файл, но в данный момент по какой-то причине происходит некоторое совпадение при чтении и записи из файла, что приводит к его бесконечному росту. Я попытаюсь обновить вопрос, не утомляя вас слишком многими деталями.
Trylks
4
Вы должны использовать временные файлы; или для входа echo HelloWorld > file; (command1<file;command2<file;command3<file)или для выхода echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Вот как это работает - вы можете форкать ввод, только если все команды работают и обрабатываются параллельно. если одна команда спит (потому что вы не хотите чередования), она просто заблокирует все команды, чтобы предотвратить заполнение памяти вводом ...
frostschutz

Ответы:

27

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

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

Писать popen(3)эти 3 командные строки оболочки, а затем freadввод и fwriteвсе три, которые будут буферизироваться до 1М.

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

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

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

Чтобы избежать одновременного запуска команд, вы можете написать 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начинает собирать данные, 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) также не открылся)

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

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=$i o$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, но

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

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

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

  1. который использует потенциально много памяти, и, кроме того, дублируя его
  2. из-за этого все 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=$i o$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
Стефан Шазелас
источник
Вы правы, тупик - самая большая проблема, которую я обнаружил до сих пор, чтобы избежать использования временных файлов. Эти файлы кажутся довольно быстрыми, хотя, я не знаю, кэшируются ли они где-нибудь, я боялся времени доступа к диску, но пока они кажутся разумными.
Trylks
6
Дополнительный +1 за хорошее искусство ASCII :-)
Курт Пфайфл
3

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

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

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

vonbrand
источник
3

Для вашей перекрывающейся проблемы в 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 будет открыт в режиме только для записи с курсором в конце файла.

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

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

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

Стефан Шазелас
источник