tee + cat: кілька разів використовуйте результат, а потім з'єднуйте результати


18

Якщо я викликаю якусь команду, наприклад, echoя можу використовувати результати цієї команди в декількох інших командах tee. Приклад:

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

З котом я можу зібрати результати декількох команд. Приклад:

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

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

Важливо , щоб зберегти результати в порядку, це означає , що лінії на виході command1, command2і 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
}

Читання та записи в ауксіфілі, здається, перекриваються, через що все вибухає.


2
Наскільки великими ми говоримо? Ваші вимоги змушують все зберігати в пам’яті. Зберігання результатів у порядку означає, що команда1 повинна спочатку виконати команду1 (імовірно, вона прочитала весь вхід і надрукувала весь вихід), перш ніж команда2 та команда3 можуть навіть почати обробку (якщо спочатку ви також не бажаєте збирати їхні дані в пам'яті).
frostschutz

Ви праві, введення та вихід команд2 та команд3 занадто великі, щоб зберігатись у пам'яті. Я очікував, що використання swap буде працювати краще, ніж використання тимчасових файлів. Ще однією проблемою є те, що це відбувається в циклі, і це ускладнює обробку файлів. Я використовую один файл, але в цей момент чомусь є певна збіга в читанні та записі з файлу, що спричиняє його зростання безмежної реклами. Я спробую оновити питання, не нудьгуючи з надто великою кількістю деталей.
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

Ви можете використовувати комбінацію stdbuf GNU і 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не відкриється інший кінець ( ))

Або уникати названих труб, трохи більш болісно з zshкопроком:

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 байт? Чергувати "абзаци", визначені якось? І якщо один просто не робить " не писати нічого довго? все це складно впоратися). Це робиться, наприклад (cmd1; cmd2; cmd3) | cmd4, програмами cmd1, cmd2і cmd3запускаються одна за одною, вихід надсилається як вхід cmd4.


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

Щоб уникнути вилки для вкладеної деякої функції, ви можете записати її як:

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

Тобто, не вкладайте перенаправлення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.