Цепочка конвейерных команд, каждая из которых выводит статус на стандартную ошибку - PullRequest
16 голосов
/ 02 мая 2020

У меня есть цепочка переданных команд в сценарии bash, передающая стандартный вывод в стандартный ввод:

prog1 | prog2 | prog3

, и каждый из них выводит что-то со стандартной ошибкой. Некоторые из них перезаписывают предыдущую строку, некоторые - нет, некоторые - и то и другое: например, выводит несколько строк, а затем обновляет «строку состояния» в оболочке. Например, curl может выводить ход загрузки в виде строки состояния.

Вывод довольно неясный, поскольку строка состояния может мерцать между выводом одного процесса и другим.

Есть ли какой-то способ чтобы сделать более понятными различные выходные данные, например,

  • Чтобы было понятно, какая строка вывода у какой программы в цепочке?
  • Чтобы сделать все строки состояния видимыми одновременно, без мерцания ?

Пример мерцания:

enter image description here

Ответы [ 3 ]

7 голосов
/ 04 мая 2020

Попробуйте это:

  1. Удалить возврат каретки из каждого вывода процесса. Иногда вам может понадобиться заменить символ возврата каретки на новую строку. Если цвет не важен, вы можете просто cat -v его.
  2. Принудительно буферизовать линии. (Это действительно только (возможно) необходимо для последней программы в конвейере, но это помогает мне отлаживать).

{ stdbuf -oL prog1 | stdbuf -oL prog2 | stdbuf -oL prog3 | stdbuf -oL tr -d '\r' ;} 2> >(stdbuf -oL tr -d '\r'>&2)

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

stdbuf -oL prog1 2> >(sed 's/\r//g; s/^/prog1: /' >&2) |
stdbuf -oL prog2 2> >(stdbuf -oL tr '\r' '\n' | sed 's/^/prog2: /' >&2) |
stdbuf -oL prog3 2> >(sed 's/\r//g; s/^/prog3: /' >&2) |
stdbuf -oL sed 's/\r//g; s/^/out: /'

Для чего-то более сложного, где вам действительно нужно разделить экран для нескольких процессов (и вы интерактивные команды) используйте screen или tmux или аналогичный для совместного использования экрана через несколько процессов или напишите свое собственное приложение, которое будет обрабатывать терминал:

tmpd=$(mktemp -d)
mkfifo "$tmpd"/1 "$tmpd"/2
trap 'rm -r "$tmpd"' EXIT
# prog1 = seq 5
# prog2 = grep -v 3
# prog3 = cat
tmux new-session \; \
  send-keys "seq 5 > $tmpd/1" C-m \; \
  split-window -v \; \
  send-keys "grep -v 3 < $tmpd/1 > $tmpd/2" C-m \; \
  split-window -v \; \
  send-keys "cat < $tmpd/2" C-m \; \
  select-layout even-vertical \;

Если, однако, вы хотите запустить программу не -взаимодействуя и все еще желая сохранить (большое) количество регистрируемой информации энергонезависимым способом, я предлагаю использовать системный регистратор, разработанный для такого случая. Из оболочки используйте logger.

$ runlog() { stdbuf -oL "$@" 2> >(logger -p local3.info -t "$1") | stdbuf -oL tee >(logger -p local3.info -t "$1"); }; 
$ runlog seq 3 | runlog grep -v 3 | runlog cat
1
2
$ sudo journalctl -p info -b0 -tseq
-- Logs begin at Fri 2018-11-02 02:06:41 CET, end at Fri 2020-05-08 14:40:24 CEST. --
maj 08 14:39:41 leonidas seq[255641]: 1
maj 08 14:39:41 leonidas seq[255641]: 2
maj 08 14:39:41 leonidas seq[255641]: 3
$ sudo journalctl -p info -b0 -tgrep
-- Logs begin at Fri 2018-11-02 02:06:41 CET, end at Fri 2020-05-08 14:40:14 CEST. --
maj 08 14:39:41 leonidas grep[255647]: 1
maj 08 14:39:41 leonidas grep[255647]: 2

В более продвинутой версии можно использовать вставные блоки fifo s и systemd, что позволит действительно точно настроить выполнение каждого исполняемого файла.

2 голосов
/ 05 мая 2020

Интересные идеи были даны здесь для этого сложного вопроса, но я не видел никакого полного решения до сих пор. Я постараюсь дать один. Чтобы достичь этого, я сначала написал три сценария, соответствующих конвейеру prog1 | prog2 | prog3, о котором говорил PO.

prog1 , производящий сообщения, разделенные \n в потоке ошибок, и генерирующий числа в стандартном потоке:

#!/bin/bash

cmd=$(basename $0)

seq 8 |
while ((i++ < 10)); do
  read line || break
  echo -e "$cmd: message $i to stderr" >&2 
  echo $line
  sleep 1
done

echo -e "$clearline$cmd: has no more input"  >&2 

prog2 , создающие сообщения, разделенные \r, и перезаписывают свой собственный вывод в потоке ошибок и передают числа из стандартного входного потока в стандартный выходной поток :

#!/bin/bash

cmd=$(basename $0)
el=$(tput el)

while ((i++ < 10)); do
  read line || break
  echo -en "$cmd: message $i to stderr${el}\r" >&2 
  echo $line
  sleep 2
done

echo -en "$clearline$cmd: has no more input${el}\r" >&2 

и, наконец, prog3 чтение из стандартного потока ввода и запись сообщений в поток ошибок таким же образом, как prog2:

#!/bin/bash

cmd=$(basename $0)
el=$(tput el)

while ((i++ < 10)); do
  read line || break
  echo -en "$cmd: message $i to stderr${el}\r" >&2 
  sleep 3
done

echo -en "$clearline$cmd: has no more input${el}\r"  >&2 

Вместо вызова этих трех сценариев как

prog1 | prog2 | prog3

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

Три панели задач находятся внизу экрана: верхняя будет содержать сообщения prog1 потоку ошибок, следующий будет соответствовать prog2, а последний внизу будет содержать сообщения от prog3.

В конце, файлы FIFO должны быть удалены.

Теперь хитрые части:

  1. Я не нашел утилиты для чтения без буферизации строки, заканчивающейся на \r, поэтому мне пришлось изменить \r на \n перед выводом строк сообщений на экран;
  2. некоторые программы в нескольких программах, которые я подключал к каналам, буферизировали их ввод или вывод, вызывая сообщения не должны быть напечатаны до конца, что явно не является предполагаемым поведением; для исправления этого я должен был использовать команду stdbuf с утилитой tr;

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

#!/bin/bash

echo -n "Test with clean output"
echo;echo;echo        # open three blank lines in the bottom of the screen
tput sc               # save the cursor position (bottom of taskbars)
l3=$(tput rc)                       # move cursor at last line of screen
l2=$(tput rc; tput cuu1)            # move cursor at second line from bottom
l1=$(tput rc; tput cuu1; tput cuu1) # move cursor at third line from bottom
el=$(tput el)         # clear to end of line
c3=$(tput setaf 1)    # set color to red
c2=$(tput setaf 2)    # set color to green
c1=$(tput setaf 3)    # set color to yellow
r0=$(tput sgr0)       # reset color

mkfifo error{1..3}    # create named pipes error1, error2 and error3

(cat error1 | stdbuf -o0 tr '\r' '\n' | 
  while read line1; do echo -en "$l1$c1$line1$el$r0"; done &)
(cat error2 | stdbuf -o0 tr '\r' '\n' | 
  while read line2; do echo -en "$l2$c2$line2$el$r0"; done &)
(cat error3 | stdbuf -o0 tr '\r' '\n' | 
  while read line3; do echo -en "$l3$c3$line3$el$r0"; done &)

./prog1 2>error1 | ./prog2  2>error2 | ./prog3 2>error3

wait

rm error{1..3}      # remove named pipes

tput rc             # put cursor below taskbars to finish gracefully
echo
echo "Test finished"

Мы добавили цвета, разные для каждой строки панели задач, со строками, созданными tput.

Наслаждайтесь.

2 голосов
/ 05 мая 2020

Поведение при перезаписи строки, вероятно, \r символов, записываемых в stderr одной или несколькими этими программами. Вот простой пример, который вы можете попробовать:

$ progress() {
  for i in {1..10}; do
    printf "$1\r" "$i" >&2; sleep 1
  done
  echo >&2
}
$ progress 'Num: %s'
# Should display a single line, `Num: N`, with `N` incrementing from 1-10

Существуют и другие способы управления курсором, такие как определенные escape-последовательности ANSI , но \r проще всего реализовать. К сожалению, поскольку вы обнаружили, что поведение не очень полезно, когда несколько программ конкурируют за одну строку, или если одновременно записано \n символов:

$ ({ sleep $(( 1+(RANDOM%8) )); echo 'Interrupt!'; } & ) &&
  progress 'Num %s' | progress '%s Something Else'
# Should see "flickering" between the two progress tasks, and eventually an "interruption"

К сожалению, нет общих -целевой способ отключить это поведение, поскольку каждая программа независимо печатает \r символов, и они не знают друг о друге. Именно по этой причине многие программы имеют некоторый механизм для отключения этого вывода в стиле прогресса, поэтому первое, что нужно искать, это флаг или параметр для его отключения, например, флаг --no_progress.

Если это Программы, которые вы написали или можете изменить, вы можете проверить, подключена ли программа к TTY или нет. В Bash это можно сделать с помощью теста -t , который может выглядеть примерно так:

$ progress() {
  for i in {1..10}; do
    # Only print progress to stderr if stdout *and* stderr are attached to TTYs
    if [[ -t 1 ]] && [[ -t 2 ]]; then
      printf "$1\r" "$i" >&2; sleep 1
    fi
  done
  echo >&2
}

Если ни один из этих подходов не осуществим, последний вариант - оберните программы и предварительно обработайте их вывод (или просто отключите stderr с помощью 2>/dev/null). Поскольку вы хотите сохранить как stdout, так и stderr, это немного сложно, но это можно сделать. Ваш помощник поменяет местами stdout и stderr , очистит stderr, например, удалив \r символов, а затем поменяет их обратно. Вот пример:

# Wraps a given command, replacing CR characters on stderr with newlines
$ no_CRs() {
  { "$@" 3>&1 1>&2 2>&3 | tr '\r' '\n'; } 3>&1 1>&2 2>&3
}

$ no_CRs progress 'Num %s' | no_CRs progress '%s Something Else'
# Should print both program's stderr on separate lines, as \r is no longer being emitted
...