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

Bash 管道命令链,每个命令将状态输出为标准错误,bash,pipe,stdout,stdin,stderr,Bash,Pipe,Stdout,Stdin,Stderr,我在bash脚本中有一系列管道命令,将标准输出管道化为标准输入: prog1 | prog2 | prog3 它们各自输出一些标准误差。有些输出覆盖了前一行,有些不覆盖,有些两者都覆盖:例如,输出几行输出,然后在shell中有一个更新的“状态栏”。例如,curl可以将下载进度作为状态栏输出 由于状态栏可能在一个进程的输出和另一个进程的输出之间闪烁,因此输出相当不清楚 是否有办法使各种输出更清晰,例如 要弄清楚输出行来自链中的哪个程序 使所有状态栏同时可见而不闪烁 闪烁的示例: 试试这个:

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

prog1 | prog2 | prog3
它们各自输出一些标准误差。有些输出覆盖了前一行,有些不覆盖,有些两者都覆盖:例如,输出几行输出,然后在shell中有一个更新的“状态栏”。例如,curl可以将下载进度作为状态栏输出

由于状态栏可能在一个进程的输出和另一个进程的输出之间闪烁,因此输出相当不清楚

是否有办法使各种输出更清晰,例如

  • 要弄清楚输出行来自链中的哪个程序
  • 使所有状态栏同时可见而不闪烁
闪烁的示例:

试试这个:

  • 从每个进程输出中删除回车。有时您可能需要用换行符替换回车符。如果颜色不重要,您只需
    cat-v
    it即可
  • 强制行缓冲。(这实际上(可能)只是管道中最后一个程序需要的,但它有助于我调试)
  • 在处理多个程序时,我通常会在每个程序的输出中添加一个标记/前缀,以便知道哪一行来自哪个程序:

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

    更高级的版本可以使用
    fifo
    s和
    systemd
    drop-in单元,这将允许对每个可执行文件的执行进行真正的微调。

    行覆盖行为可能是这些程序中的一个或多个将
    \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
    
    还有其他方法可以控制光标,例如某些方法,但是
    \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中,这可以通过完成,它可能看起来像这样:

    $ 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>/dev/null
    抑制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
    

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

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

    #/bin/bash
    cmd=$(basename$0)
    序号8|
    而((i++<10));做
    读线| |断开
    echo-e“$cmd:message$i to stderr”>&2
    回音$线
    睡眠1
    完成
    echo-e“$clearline$cmd:没有更多输入”>&2
    
    prog2生成由
    \r
    分隔的消息,并在错误流上覆盖其自身的输出,并将数字从标准输入流传输到标准输出流:

    #/bin/bash
    cmd=$(basename$0)
    el=$(t输出el)
    而((i++<10));做
    读线| |断开
    echo-en“$cmd:message$i to stderr${el}\r”>&2
    回音$线
    睡眠2
    完成
    echo-en“$clearline$cmd:没有更多输入${el}\r”>&2
    
    最后,prog3以与
    prog2
    相同的方式读取标准输入流并将消息写入错误流:

    #/bin/bash
    cmd=$(basename$0)
    el=$(t输出el)
    而((i++<10));做
    读线| |断开
    echo-en“$cmd:message$i to stderr${el}\r”>&2
    睡眠3
    完成
    echo-en“$clearline$cmd:没有更多输入${el}\r”>&2
    
    而不是将这三个脚本作为

    prog1 | prog2 | prog3
    
    我们需要一个脚本来调用这三个程序,将错误流重定向到三个FIFO特殊文件(命名管道),但在启动此命令之前,我们必须先创建三个特殊文件,并在后台启动进程来侦听特殊文件:每次发送一整行,这些进程将把它打印在屏幕的一个特殊区域,我称之为任务栏

    三个任务栏位于屏幕底部:上面的任务栏将包含错误流的
    prog1
    消息,下一个任务栏将对应于
    prog2
    ,底部的最后一个任务栏将包含来自
    prog3
    的消息

    最后,必须删除FIFO文件

    现在是棘手的部分:

  • 我发现没有缓冲以
    \r
    结尾的行的实用程序读取,因此在将消息行打印到屏幕之前,我必须将
    \r
    更改为
    \n
  • 我与管道连接的几个程序中的一些程序缓冲了它们的输入或输出,从而产生了消息
    $ 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
    }
    
    # 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