获取通过管道传输到另一个进程的退出状态

获取通过管道传输到另一个进程的退出状态

我有两个进程foobar,通过管道连接:

$ foo | bar

bar总是退出 0;我对 的退出代码很感兴趣foo。有什么办法可以得到它吗?

答案1

bashzsh有一个数组变量,用于保存 shell 执行的最后一个管道的每个元素(命令)的退出状态。

如果您使用bash,则调用该数组PIPESTATUS(区分大小写!)并且数组索引从零开始:

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

如果您使用zsh,则调用该数组pipestatus(区分大小写!)并且数组索引从 1 开始:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

要以不丢失值的方式将它们组合在函数中:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

bash在or中运行上面的代码zsh,你会得到相同的结果;仅设置retval_bash和之一。retval_zsh另一个将是空白的。这将允许函数以结尾return $retval_bash $retval_zsh(注意缺少引号!)。

答案2

有 3 种常见方法可以实现此目的:

管道故障

第一种方法是设置pipefail选项(kshzshbash)。这是最简单的,它所做的基本上是将退出状态设置$?为最后一个程序的退出代码以非零退出(如果全部成功退出则为零)。

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$管道状态

Bash 还有一个名为$PIPESTATUS( $pipestatusin zsh) 的数组变量,其中包含最后一个管道中所有程序的退出状态。

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

您可以使用第三个命令示例来获取管道中所需的特定值。

分开处决

这是最笨拙的解决方案。单独运行每个命令并捕获状态

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

答案3

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

情况:

someprog | filter

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

这是我的解决方案:

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

该构造的结果是来自filter该构造的 stdout 的 stdout 和来自someprog该构造的退出状态的退出状态。


此结构还适用于简单的命令分组{...}而不是子 shell (...)。子 shell 有一些影响,其中包括性能成本,我们在这里不需要。阅读精美的 bash 手册以获取更多详细信息:https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

不幸的是,bash 语法需要大括号中的空格和分号,以便结构变得更加宽敞。

对于本文的其余部分,我将使用 subshel​​l 变体。


示例someprogfilter

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

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

echo $?

输出示例:

filtered line1
filtered line2
filtered line3
42

注意:子进程从父进程继承打开的文件描述符。这意味着someprog将继承打开的文件描述符 3 和 4。如果someprog写入文件描述符 3 那么这将成为退出状态。真正的退出状态将被忽略,因为read只读取一次。

如果您担心someprog可能会写入文件描述符 3 或 4,那么最好在调用之前关闭文件描述符someprog

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

before在执行exec 3>&- 4>&-之前someprog关闭文件描述符someprog,因此someprog这些文件描述符根本不存在。

也可以这样写:someprog 3>&- 4>&-


构造的逐步解释:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

从下往上:

  1. 使用重定向到 stdout 的文件描述符 4 创建子 shell。这意味着子 shell 中打印到文件描述符 4 的任何内容最终都将作为整个构造的标准输出。
  2. 创建管道并执行左侧 ( #part3) 和右侧 ( ) 的命令。也是管道的最后一个命令,这意味着来自 stdin 的字符串将是整个构造的退出状态。#part2exit $xs
  3. 使用重定向到 stdout 的文件描述符 3 创建子 shell。这意味着在此子 shell 中打印到文件描述符 3 的任何内容都将最终#part2成为整个构造的退出状态。
  4. 创建管道并执行左侧 (#part5#part6) 和右侧 ( ) 的命令。filter >&4的输出filter被重定向到文件描述符4。在#part1文件描述符4中被重定向到stdout。这意味着 的输出filter是整个构造的标准输出。
  5. 退出状态 from#part6打印到文件描述符 3。在#part3文件描述符 3 中,重定向到#part2。这意味着退出状态#part6将是整个构造的最终退出状态。
  6. someprog被执行。退出状态被接收#part5。标准输出由管道 in 获取#part4并转发到filter。的输出filter将依次到达标准输出,如中所述#part4

答案4

如果可能的话,我所做的是将退出代码输入foobar.例如,如果我知道foo永远不会生成仅包含数字的行,那么我可以添加退出代码:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

或者,如果我知道输出从不foo包含仅包含以下内容的行.

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

如果有某种方法可以bar处理除最后一行之外的所有行,并将最后一行作为其退出代码传递,则始终可以完成此操作。

如果bar是一个复杂的管道,您不需要其输出,则可以通过在不同的文件描述符上打印退出代码来绕过它的一部分。

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

在此之后$exit_codes通常是,但如果在读取所有输入之前退出或者如果您不走运,则foo:X bar:Y可能会出现这种情况。我认为在所有 unice 上写入最多 512 字节的管道都是原子的,因此只要标记字符串低于 507 字节,和部分就不会混合。bar:Y foo:Xbarfoo:$?bar:$?

如果您需要捕获 的输出bar,就会变得很困难。您可以通过安排 never 的输出来组合上述技术,bar使其包含看起来像退出代码指示的行,但它确实变得很繁琐。

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

当然,还有一个简单的选择使用临时文件来存储状态。简单,但不制作简单:

  • 如果有多个脚本同时运行,或者同一个脚本在多个地方使用此方法,则需要确保它们使用不同的临时文件名。
  • 在共享目录中安全地创建临时文件很困难。通常,/tmp这是脚本确定能够写入文件的唯一地方。使用mktemp,这不是 POSIX,但现在所有严肃的 unice 上都可用。
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

相关内容