【问题标题】:Pipe output and capture exit status in Bash在 Bash 中管道输出和捕获退出状态
【发布时间】:2010-11-16 08:22:36
【问题描述】:

我想在 Bash 中执行一个长时间运行的命令,同时捕获它的退出状态和 tee 它的输出。

所以我这样做:

command | tee out.txt
ST=$?

问题在于变量 ST 捕获了tee 的退出状态,而不是命令的退出状态。我该如何解决这个问题?

请注意,命令运行时间很长,并且将输出重定向到文件以便稍后查看对我来说不是一个好的解决方案。

【问题讨论】:

  • [[ "${PIPESTATUS[@]}" =~ [^0\ ] ]] && echo -e "匹配 - 发现错误" || echo -e "No match - all good" 这将一次测试数组的所有值,如果返回的任何管道值不为零,则会给出错误消息。这是一个非常强大的通用解决方案,用于检测管道情况下的错误。

标签: bash shell error-handling pipe


【解决方案1】:

有一个名为$PIPESTATUS的内部Bash变量;它是一个数组,用于保存最后一个前台命令管道中每个命令的退出状态。

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

或者也可以与其他 shell(如 zsh)一起使用的另一种选择是启用 pipefail:

set -o pipefail
...

第一个选项 zsh 一起使用,因为语法略有不同。

【讨论】:

  • 这里有 PIPESTATUS 和 Pipefail 示例的很好解释:unix.stackexchange.com/a/73180/7453
  • 注意:$PIPESTATUS[0] 保存管道中第一个命令的退出状态,$PIPESTATUS[1] 保存第二个命令的退出状态,以此类推。
  • 当然,我们必须记住这是特定于 Bash 的:如果我要(例如)编写一个脚本以在我的 Android 设备或其他设备上运行 BusyBox 的“sh”实现使用其他“sh”变体的嵌入式平台,这是行不通的。
  • 对于那些关心未引用变量扩展的人:退出状态始终为无符号8位整数in Bash,因此无需引用它。这通常在 Unix 下也成立,exit status is defined to be 8-bit explicitly,即使 POSIX 本身也假定它是未签名的,例如定义其logical negation时。
  • 你也可以使用exit ${PIPESTATUS[0]}
【解决方案2】:

愚蠢的解决方案:通过命名管道 (mkfifo) 连接它们。然后可以第二次运行命令。

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

【讨论】:

  • 这是这个问题中唯一适用于简单 sh Unix shell 的答案。谢谢!
  • @DaveKennedy:就像“显而易见,不需要复杂的 bash 语法知识”一样愚蠢
  • 当您利用 bash 的额外功能时,bash 的答案会更加优雅,但这是更跨平台的解决方案。一般来说,这也是值得考虑的事情,因为每当您执行长时间运行的命令时,名称管道通常是最灵活的方式。值得注意的是,有些系统没有mkfifo,如果我没记错的话,可能需要mknod -p
  • 有时在堆栈溢出时,有些答案你会赞成一百次,这样人们就会停止做其他没有意义的事情,这就是其中之一。谢谢先生。
【解决方案3】:

使用 bash 的 set -o pipefail 很有帮助

pipefail:管道的返回值是状态 最后一个以非零状态退出的命令, 如果没有命令以非零状态退出,则为零

【讨论】:

  • 如果不想修改整个脚本的pipefail设置,可以只在本地设置选项:( set -o pipefail; command | tee out.txt ); ST=$?
  • @Jaan 这将运行一个子shell。如果您想避免这种情况,您可以先执行set -o pipefail,然后执行命令,然后立即执行set +o pipefail 以取消设置该选项。
  • 注意:问题发布者不想要管道的“一般退出代码”,他想要“命令”的返回代码。使用-o pipefail,他会知道管道是否失败,但如果“command”和“tee”都失败,他会收到来自“tee”的退出代码。
  • @LinusArver 不会清除退出代码,因为它是一个成功的命令?
【解决方案4】:

有一个数组可以为您提供管道中每个命令的退出状态。

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1

【讨论】:

    【解决方案5】:

    此解决方案无需使用 bash 特定功能或临时文件即可工作。奖励:最后退出状态实际上是退出状态,而不是文件中的某个字符串。

    情况:

    someprog | filter
    

    您想要someprog 的退出状态和filter 的输出。

    这是我的解决方案:

    ((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
    
    echo $?
    

    请参阅my answer for the same question on unix.stackexchange.com 以获取详细说明以及不带子外壳和一些警告的替代方案。

    【讨论】:

      【解决方案6】:

      通过结合PIPESTATUS[0]和在子shell中执行exit命令的结果,你可以直接访问你的初始命令的返回值:

      command | tee ; ( exit ${PIPESTATUS[0]} )

      这是一个例子:

      # the "false" shell built-in command returns 1
      false | tee ; ( exit ${PIPESTATUS[0]} )
      echo "return value: $?"
      

      会给你:

      return value: 1

      【讨论】:

      • 谢谢,这允许我使用构造:VALUE=$(might_fail | piping),它不会在主 shell 中设置 PIPESTATUS,但会设置其错误级别。通过使用:VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]}) 我得到我想要的。
      • @vaab,该语法看起来非常好,但我对“管道”在您的上下文中的含义感到困惑?那只是一个人会做'tee'或对may_fail的输出进行任何处理的地方吗?太棒了!
      • @AnneTheAgile 'piping' 在我的示例中代表您不希望从中看到 errlvl 的命令。例如:'tee','grep','sed',......其中一个或任何管道组合,这些管道命令用于格式化或从主要输出或日志输出中提取信息并不少见命令:然后您对主命令的 errlevel 更感兴趣(我在示例中称为“might_fail”),但没有我的构造,整个分配将返回最后一个管道命令的 errlvl,这在这里毫无意义。这更清楚了吗?
      • command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]} 如果不是 tee 而是 grep 过滤
      【解决方案7】:

      所以我想贡献一个像 lesmana's 这样的答案,但我认为我的可能是一个更简单、更有利的纯 Bourne-shell 解决方案:

      # You want to pipe command1 through command2:
      exec 4>&1
      exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
      # $exitstatus now has command1's exit status.
      

      我认为最好从里到外解释——command1 将执行并在 stdout(文件描述符 1)上打印其常规输出,然后一旦完成,printf 将执行并在其 stdout 上打印 icommand1 的退出代码,但那个 stdout被重定向到文件描述符 3。

      当 command1 运行时,它的 stdout 被传送到 command2(printf 的输出永远不会传送到 command2,因为我们将它发送到文件描述符 3 而不是 1,这是管道读取的内容)。然后我们将 command2 的输出重定向到文件描述符 4,这样它也不会出现在文件描述符 1 之外——因为我们希望稍后释放文件描述符 1,因为我们会将文件描述符 3 上的 printf 输出带回到文件描述符中1 - 因为这是命令替换(反引号)将捕获的内容,这就是将被放入变量中的内容。

      最后一点神奇之处在于,我们首先将exec 4&gt;&amp;1 作为单独的命令执行 - 它打开文件描述符 4 作为外部 shell 标准输出的副本。命令替换将从其内部命令的角度捕获标准输出中写入的任何内容 - 但由于 command2 的输出就命令替换而言将发送到文件描述符 4,因此命令替换不会捕获它 - 但是一旦它从命令替换中“退出”它实际上仍将转到脚本的整体文件描述符 1。

      (exec 4&gt;&amp;1 必须是一个单独的命令,因为当您尝试在命令替换中写入文件描述符时,许多常见的 shell 不喜欢它,这是在使用替换。所以这是最简单的可移植方式。)

      你可以用一种不那么技术性和更有趣的方式来看待它,就好像命令的输出是相互跳跃的:command1 管道到 command2,然后 printf 的输出跳过命令 2,这样 command2 就不会捕获它,然后命令 2 的输出跳过并跳出命令替换,就像 printf 及时着陆以被替换捕获,因此它最终出现在变量中,并且命令 2 的输出继续以愉快的方式被写入标准输出,就像在普通管道中一样。

      另外,据我了解,$? 仍将包含管道中第二个命令的返回码,因为变量赋值、命令替换和复合命令对于其中的命令的返回码都是有效透明的,因此 command2 的返回状态应该被传播出去 - 这就是为什么我认为这可能是比 lesmana 提出的解决方案更好的解决方案。

      根据 lesmana 提到的注意事项,command1 可能会在某些时候最终使用文件描述符 3 或 4,因此为了更健壮,您可以这样做:

      exec 4>&1
      exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
      exec 4>&-
      

      请注意,我在示例中使用了复合命令,但子 shell(使用 ( ) 代替 { } 也可以,但可能效率较低。)

      命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符四,而3&gt;&amp;1 后面的复合命令将继承文件描述符三。所以4&gt;&amp;- 确保内部复合命令不会继承文件描述符四,3&gt;&amp;- 不会继承文件描述符三,因此 command1 获得了一个“更干净”、更标准的环境。你也可以将内部的4&gt;&amp;- 移动到3&gt;&amp;- 旁边,但我想为什么不尽可能地限制它的范围。

      我不确定直接使用文件描述符 3 和 4 的频率 - 我认为大多数时候程序使用返回未使用的文件描述符的系统调用,但有时代码写入文件描述符 3直接,我猜(我可以想象一个程序检查文件描述符以查看它是否打开,如果是则使用它,或者如果不是,则相应地表现不同)。所以后者可能最好记住并用于通用情况。

      【讨论】:

      • 很好的解释!
      【解决方案8】:

      在 Ubuntu 和 Debian 中,您可以apt-get install moreutils。这包含一个名为 mispipe 的实用程序,它返回管道中第一个命令的退出状态。

      【讨论】:

        【解决方案9】:
        (command | tee out.txt; exit ${PIPESTATUS[0]})
        

        与@cODAR 的答案不同,它返回第一个命令的原始退出代码,而不仅仅是 0 表示成功,127 表示失败。但正如@Chaoran 指出的那样,您可以致电${PIPESTATUS[0]}。重要的是,所有内容都放在括号中。

        【讨论】:

          【解决方案10】:

          在 bash 之外,你可以这样做:

          bash -o pipefail  -c "command1 | tee output"
          

          这在 shell 应该是 /bin/sh 的忍者脚本中很有用。

          【讨论】:

            【解决方案11】:

            在普通 bash 中执行此操作的最简单方法是使用 process substitution 而不是管道。有几个不同之处,但它们对您的用例可能并不重要:

            • 运行管道时,bash 会一直等待,直到所有进程完成。
            • 向 bash 发送 Ctrl-C 会使其杀死管道的所有进程,而不仅仅是主要进程。
            • pipefail 选项和PIPESTATUS 变量与进程替换无关。
            • 可能更多

            使用进程替换,bash 只是启动进程并忘记它,它甚至在 jobs 中都不可见。

            除了提到的差异之外,consumer &lt; &lt;(producer)producer | consumer 本质上是等价的。

            如果要翻转哪个是“主”进程,只需将命令和替换方向翻转为producer &gt; &gt;(consumer)。在你的情况下:

            command > >(tee out.txt)
            

            例子:

            $ { echo "hello world"; false; } > >(tee out.txt)
            hello world
            $ echo $?
            1
            $ cat out.txt
            hello world
            
            $ echo "hello world" > >(tee out.txt)
            hello world
            $ echo $?
            0
            $ cat out.txt
            hello world
            

            正如我所说,管道表达式存在差异。该过程可能永远不会停止运行,除非它对管道关闭很敏感。特别是,它可能会不断向您的标准输出写入内容,这可能会造成混淆。

            【讨论】:

            • 这是我的首选解决方案。
            【解决方案12】:

            PIPESTATUS[@] 必须在管道命令返回后立即复制到数组中。 任何对 PIPESTATUS[@] 的读取都会擦除内容。 如果您计划检查所有管道命令的状态,请将其复制到另一个数组。 “美元?”与“${PIPESTATUS[@]}”的最后一个元素的值相同, 并且阅读它似乎会破坏“${PIPESTATUS[@]}”,但我还没有完全验证这一点。

            declare -a PSA  
            cmd1 | cmd2 | cmd3  
            PSA=( "${PIPESTATUS[@]}" )
            

            如果管道位于子外壳中,这将不起作用。为了解决这个问题,
            bash pipestatus in backticked command?

            【讨论】:

              【解决方案13】:

              纯shell解决方案:

              % rm -f error.flag; echo hello world \
              | (cat || echo "First command failed: $?" >> error.flag) \
              | (cat || echo "Second command failed: $?" >> error.flag) \
              | (cat || echo "Third command failed: $?" >> error.flag) \
              ; test -s error.flag  && (echo Some command failed: ; cat error.flag)
              hello world
              

              现在将第二个cat 替换为false

              % rm -f error.flag; echo hello world \
              | (cat || echo "First command failed: $?" >> error.flag) \
              | (false || echo "Second command failed: $?" >> error.flag) \
              | (cat || echo "Third command failed: $?" >> error.flag) \
              ; test -s error.flag  && (echo Some command failed: ; cat error.flag)
              Some command failed:
              Second command failed: 1
              First command failed: 141
              

              请注意第一只猫也失败了,因为它的标准输出被关闭了。在这个例子中,日志中失败命令的顺序是正确的,但不要依赖它。

              此方法允许捕获单个命令的 stdout 和 stderr,因此您可以在发生错误时将其转储到日志文件中,或者在没有错误时将其删除(如 dd 的输出)。

              【讨论】:

                【解决方案14】:

                基于@brian-s-wilson 的回答;这个 bash 辅助函数:

                pipestatus() {
                  local S=("${PIPESTATUS[@]}")
                
                  if test -n "$*"
                  then test "$*" = "${S[*]}"
                  else ! [[ "${S[@]}" =~ [^0\ ] ]]
                  fi
                }
                

                这样使用:

                1:get_bad_things 必须成功,但不会产生任何输出;但我们希望看到它确实产生的输出

                get_bad_things | grep '^'
                pipeinfo 0 1 || return
                

                2:所有管道都必须成功

                thing | something -q | thingy
                pipeinfo || return
                

                【讨论】:

                  【解决方案15】:

                  有时使用外部命令可能更简单、更清晰,而不是深入研究 bash 的细节。 pipeline,来自最小进程脚本语言execline,以第二个命令的返回码*退出,就像sh 管道一样,但与sh 不同,它允许反转管道的方向,所以我们可以捕获生产者进程的返回码(以下全部在sh命令行上,但安装了execline):

                  $ # using the full execline grammar with the execlineb parser:
                  $ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
                  hello world
                  $ cat out.txt
                  hello world
                  
                  $ # for these simple examples, one can forego the parser and just use "" as a separator
                  $ # traditional order
                  $ pipeline echo "hello world" "" tee out.txt 
                  hello world
                  
                  $ # "write" order (second command writes rather than reads)
                  $ pipeline -w tee out.txt "" echo "hello world"
                  hello world
                  
                  $ # pipeline execs into the second command, so that's the RC we get
                  $ pipeline -w tee out.txt "" false; echo $?
                  1
                  
                  $ pipeline -w tee out.txt "" true; echo $?
                  0
                  
                  $ # output and exit status
                  $ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
                  hello world
                  RC: 42
                  $ cat out.txt
                  hello world
                  

                  使用 pipeline 与本机 bash 管道的区别与答案 #43972501 中使用的 bash 进程替换相同。

                  * 实际上pipeline 根本不会退出,除非出现错误。它执行到第二个命令,所以返回的是第二个命令。

                  【讨论】:

                    猜你喜欢
                    • 2012-10-15
                    • 1970-01-01
                    • 2015-05-08
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-10-24
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多