代码之家  ›  专栏  ›  技术社区  ›  hugomg

捕捉壳管中的错误代码

  •  80
  • hugomg  · 技术社区  · 15 年前

    ./a | ./b | ./c
    

    我想对其进行修改,以便如果a、b或c中的任何一个退出时带有错误代码,我将打印一条错误消息并停止,而不是将错误输出向前传输。

    最简单/最干净的方法是什么?

    4 回复  |  直到 6 年前
        1
  •  172
  •   Markus Unterwaditzer    10 年前

    在里面 你可以用 set -e set -o pipefail 在文件的开头。后续命令 ./a | ./b | ./c 当三个脚本中的任何一个失败时,将失败。返回代码将是第一个失败脚本的返回代码。

    注意 pipefail .

        2
  •  51
  •   sanmai    8 年前

    ${PIPESTATUS[]} 完全执行后的数组,例如,如果运行:

    ./a | ./b | ./c
    

    ${PIPESTATUS} 将是管道中每个命令的错误代码数组,因此如果中间命令失败, echo ${PIPESTATUS[@]} 将包含以下内容:

    0 1 0
    

    类似这样的命令在命令后运行:

    test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0
    

    将允许您检查管道中的所有命令是否成功。

        3
  •  22
  •   Jonathan Leffler vy32    15 年前

    如果您确实不希望第二个命令在第一个命令成功之前继续执行,那么您可能需要使用临时文件。简单的版本是:

    tmp=${TMPDIR:-/tmp}/mine.$$
    if ./a > $tmp.1
    then
        if ./b <$tmp.1 >$tmp.2
        then
            if ./c <$tmp.2
            then : OK
            else echo "./c failed" 1>&2
            fi
        else echo "./b failed" 1>&2
        fi
    else echo "./a failed" 1>&2
    fi
    rm -f $tmp.[12]
    

    "1"及&2“重定向也可以缩写”>&2'; 然而,MKS外壳的旧版本在没有前面的“1”的情况下错误地处理了错误重定向,因此我多年来一直使用这种明确的可靠性表示法。

    如果您中断了某些内容,则会泄漏文件。防爆(或多或少)外壳编程使用:

    tmp=${TMPDIR:-/tmp}/mine.$$
    trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
    ...if statement as before...
    rm -f $tmp.[12]
    trap 0 1 2 3 13 15
    

    第一行陷阱是“运行命令” rm -f $tmp.[12]; exit 1 '当信号1 SIGHUP、2 SIGINT、3 SIGQUIT、13 SIGPIPE或15 SIGTERM中的任何一个出现时,或0(当外壳因任何原因退出时)。 如果您正在编写shell脚本,则最终陷阱只需删除0上的陷阱,即shell退出陷阱(由于进程即将终止,您可以保留其他信号)。

    在原始管道中,“c”在“a”完成之前从“b”读取数据是可行的——这通常是可取的(例如,它提供了多个核的工作)。如果“b”是一个“排序”阶段,那么这将不适用-“b”必须先查看其所有输入,然后才能生成任何输出。

    如果要检测哪些命令失败,可以使用:

    (./a || echo "./a exited with $?" 1>&2) |
    (./b || echo "./b exited with $?" 1>&2) |
    (./c || echo "./c exited with $?" 1>&2)
    

    这是简单和对称的-扩展到4部分或N部分管道很简单。

    简单的“set-e”实验没有帮助。

        4
  •  9
  •   josch    8 年前

    不幸的是,Johnathan的答案需要临时文件,Michel和Imron的答案需要bash(尽管这个问题被标记为shell)。正如其他人已经指出的,在以后的进程开始之前,不可能中止管道。所有进程都是一次启动的,因此在传递任何错误之前,所有进程都将运行。但问题的标题也是关于错误代码的问题。管道完工后,可以检索和调查这些数据,以确定是否有任何相关过程失败。

    下面是一个解决方案,它捕获管道中的所有错误,而不仅仅是最后一个组件的错误。这就像bash的pipefail,只是在您可以检索的意义上更强大 全部的 错误代码。

    res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
    (./b 2>&1 || echo "2nd failed with $?" >&2) |
    (./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
    if [ -n "$res" ]; then
        echo pipe failed
    fi
    

    为了检测是否有任何故障,需要一个 echo 如果任何命令失败,命令按标准错误打印。然后,组合标准误差输出保存在 $res 后来调查。这也是将所有进程的标准错误重定向到标准输出的原因。您还可以将该输出发送到 /dev/null 或者把它当作是出了问题的另一个指示器。您可以将最后一个重定向替换为 /dev/null 如果您需要将最后一个命令的输出存储在任何位置,则使用文件。

    为了更多地使用这个构造,并让自己相信它确实做到了它应该做的事情,我替换了 ./a ./b ./c 通过执行 回响 , cat exit . 您可以使用它来检查这个构造是否真的将所有输出从一个进程转发到另一个进程,以及错误代码是否被正确记录。

    res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
    (sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
    (sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
    if [ -n "$res" ]; then
        echo pipe failed
    fi
    
        5
  •  1
  •   Jasha    3 年前

    这个答案符合公认答案的精神,但使用shell变量而不是临时文件。

    if TMP_A="$(./a)"
    then
     if TMP_B="$(echo "TMP_A" | ./b)"
     then
      if TMP_C="$(echo "TMP_B" | ./c)"
      then
       echo "$TMP_C"
      else
       echo "./c failed"
      fi
     else
      echo "./b failed"
     fi
    else
     echo "./a failed"
    fi