【问题标题】:Chain of piped commands, each outputting status to standard error管道命令链,每个输出状态到标准错误
【发布时间】:2020-08-16 17:07:32
【问题描述】:

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

prog1 | prog2 | prog3

它们每个都向标准错误输出一些东西。其中一些输出覆盖前一行,一些没有,一些两者都做:例如输出几行输出,然后在 shell 中有一个更新的“状态栏”。例如,curl 可以将下载进度输出为状态栏。

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

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

  • 要明确哪个输出行来自链中的哪个程序?
  • 要让所有状态栏同时可见而不闪烁?

闪烁示例:

【问题讨论】:

  • Idk,也许您可​​以将每个单独程序的 stderror 重定向到不同的命名管道,然后以某种方式最后将它们全部组合起来?但是结合覆盖自身的错误输出听起来很冒险。
  • 您真的需要每个程序的输出,还是使用pv 之类的工具来监控流经管道的数据量是否足够?
  • @Martin 通过管道监控数据量/速率会有所帮助,但最终,我希望应用程序特定的更新更清晰。

标签: bash pipe stdout stdin stderr


【解决方案1】:

试试这个:

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

对于您确实需要为多个进程共享屏幕(并且您正在交互式运行命令)的任何更复杂的事情,请使用 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 "$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

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

【讨论】:

    【解决方案2】:

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

    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. 我用管道连接的几个程序中的一些程序正在缓冲它们的输入或输出,导致消息直到最后才被打印,这显然不是预期的行为;为了解决这个问题,我必须使用命令 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 生成。

    享受吧。

    【讨论】:

    • @MaximEgorushkin:您建议在哪里添加对stdbuf 的呼叫?我试验了一下,只在tr之前需要。
    【解决方案3】:

    行覆盖行为可能是\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
    

    还有其他方法可以控制光标,比如某些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 "$1\r" "$i" >&2; sleep 1
        fi
      done
      echo >&2
    }
    

    如果这两种方法都不可行,最后一个选择是包装程序并预处理它们的输出(或者简单地使用2&gt;/dev/null 抑制标准错误)。由于您想同时保留 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
    

    【讨论】:

      猜你喜欢
      • 2018-10-09
      • 2017-08-11
      • 2015-04-25
      • 1970-01-01
      • 2021-04-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多