管道命令链,每个输出状态到标准错误

Chain of piped commands, each outputting status to standard error

我在 bash 脚本中有一个管道命令链,将标准输出管道传输到标准输入:

prog1 | prog2 | prog3

他们每个人都输出一些东西到标准错误。其中一些输出覆盖前一行,一些不覆盖,一些两者都覆盖:例如输出几行输出,然后在shell中有一个更新"status bar"。例如,curl 可以将下载进度输出为状态栏。

输出相当不清楚,因为状态栏会在一个进程的输出与另一个进程的输出之间闪烁。

有没有办法让各种输出更清晰,例如

闪烁的例子:

试试这个:

  1. 从每个进程输出中删除回车符 return。有时您可能需要用回车符 return 替换换行符。如果颜色不重要,你可以cat -v它。
  2. 强制行缓冲。 (这真的只是(可能)管道中最后一个程序需要的,但它有助于我调试)。

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

在处理多个程序时,我通常会在它们的每个输出中添加一个tag/prefix,这样我就知道哪一行来自哪个程序:

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: /'

对于真正需要为多个进程共享屏幕的更复杂的情况(并且您正在 运行 宁命令交互)使用 screentmux 或类似的共享通过多个进程筛选或编写您自己的应用程序来处理终端:

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 \;

但是,如果您的目标是 运行 非交互式程序并且仍然希望以非易失性方式保留(大量)日志信息,我建议使用专为此类设计的系统日志记录器案件。从 shell 使用 logger.

$ runlog() { stdbuf -oL "$@" 2> >(logger -p local3.info -t "") | stdbuf -oL tee >(logger -p local3.info -t ""); }; 
$ 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

更高级的版本可以使用 fifos 和 systemd 插入单元,这将允许真正微调每个可执行文件的执行。

行覆盖行为可能是 \r 个字符被这些程序中的一个或多个写入 stderr。这是一个您可以尝试的简单示例:

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

还有其他方法可以控制光标,比如某些ANSI escape sequences,但是\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 test 来完成,它可能看起来像这样:

$ 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 "\r" "$i" >&2; sleep 1
    fi
  done
  echo >&2
}

如果这些方法都不可行,最后一个选择是包装程序并预处理它们的输出(或者简单地用 2>/dev/null 抑制 stderr)。由于您想同时保留 stdout 和 stderr,这有点繁琐,但可以做到。您的助手会 swap stdout and 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

对于这个具有挑战性的问题,这里给出了有趣的想法,但到目前为止我还没有看到任何完整的解决方案。我会尝试给一个。为了实现这一点,我首先编写了三个脚本,对应于 PO 所说的管道 prog1 | prog2 | prog3

prog1 在错误流上生成由 \n 分隔的消息并在标准流上生成数字:

#!/bin/bash

cmd=$(basename [=10=])

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 [=11=])
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 [=12=])
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. 我用管道连接的几个程序中的一些程序正在缓冲它们的输入或输出,导致消息直到最后才打印,这显然不是预期的行为;为了解决这个问题,我不得不将命令 stdbuftr 实用程序一起使用;

综上所述,我实现了下一个脚本,它按预期工作:

#!/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 生成。

尽情享受吧。