我有两个进程foo
和bar
,通过管道连接:
$ foo | bar
bar
总是退出 0;我对 的退出代码很感兴趣foo
。有什么办法可以得到它吗?
答案1
bash
并zsh
有一个数组变量,用于保存 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
选项(ksh
、zsh
或bash
)。这是最简单的,它所做的基本上是将退出状态设置$?
为最后一个程序的退出代码以非零退出(如果全部成功退出则为零)。
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
$管道状态
Bash 还有一个名为$PIPESTATUS
( $pipestatus
in 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 语法需要大括号中的空格和分号,以便结构变得更加宽敞。
对于本文的其余部分,我将使用 subshell 变体。
示例someprog
和filter
:
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
从下往上:
- 使用重定向到 stdout 的文件描述符 4 创建子 shell。这意味着子 shell 中打印到文件描述符 4 的任何内容最终都将作为整个构造的标准输出。
- 创建管道并执行左侧 (
#part3
) 和右侧 ( ) 的命令。也是管道的最后一个命令,这意味着来自 stdin 的字符串将是整个构造的退出状态。#part2
exit $xs
- 使用重定向到 stdout 的文件描述符 3 创建子 shell。这意味着在此子 shell 中打印到文件描述符 3 的任何内容都将最终
#part2
成为整个构造的退出状态。 - 创建管道并执行左侧 (
#part5
和#part6
) 和右侧 ( ) 的命令。filter >&4
的输出filter
被重定向到文件描述符4。在#part1
文件描述符4中被重定向到stdout。这意味着 的输出filter
是整个构造的标准输出。 - 退出状态 from
#part6
打印到文件描述符 3。在#part3
文件描述符 3 中,重定向到#part2
。这意味着退出状态#part6
将是整个构造的最终退出状态。 someprog
被执行。退出状态被接收#part5
。标准输出由管道 in 获取#part4
并转发到filter
。的输出filter
将依次到达标准输出,如中所述#part4
答案4
如果可能的话,我所做的是将退出代码输入foo
到bar
.例如,如果我知道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:X
bar
foo:$?
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")