我有两个进程foo和bar,用管道连接:
$ foo | bar
Run Code Online (Sandbox Code Playgroud)
bar总是退出 0;我对foo. 有什么办法可以得到吗?
cam*_*amh 351
bash并zsh有一个数组变量,用于保存 shell 执行的最后一个管道的每个元素(命令)的退出状态。
如果您使用bash,则调用数组PIPESTATUS(大小写很重要!)并且数组索引从零开始:
$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0
Run Code Online (Sandbox Code Playgroud)
如果您使用zsh,则调用数组pipestatus(大小写很重要!)并且数组索引从 1 开始:
$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0
Run Code Online (Sandbox Code Playgroud)
以不丢失值的方式将它们组合在一个函数中:
$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0
Run Code Online (Sandbox Code Playgroud)
在bashor 中运行上面的代码zsh,你会得到相同的结果;只有其中之一retval_bash和retval_zsh将被设置。另一个将是空白的。这将允许函数以return $retval_bash $retval_zsh(注意缺少引号!)结尾。
phe*_*mer 301
有3种常见的方法来做到这一点:
第一种方法是设置pipefail选项(ksh,zsh或bash)。这是最简单的,它所做的基本上是将退出状态设置为$?最后一个程序的退出代码以退出非零(如果全部成功退出,则为零)。
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
您可以使用第三个命令示例来获取您需要的管道中的特定值。
这是最笨拙的解决方案。分别运行每个命令并捕获状态
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
Run Code Online (Sandbox Code Playgroud)
les*_*ana 63
此解决方案无需使用 bash 特定功能或临时文件即可工作。奖励:最终退出状态实际上是退出状态,而不是文件中的某个字符串。
情况:
someprog | filter
Run Code Online (Sandbox Code Playgroud)
你想要退出状态someprog和输出filter。
这是我的解决方案:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
Run Code Online (Sandbox Code Playgroud)
这个构造的结果是filter作为构造的标准输出的标准输出和作为构造的退出状态的someprog退出状态。
此构造也适用于简单的命令分组{...}而不是 subshells (...)。subshells 有一些影响,其中包括性能成本,我们在这里不需要。阅读精美的 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
Run Code Online (Sandbox Code Playgroud)
不幸的是,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 $?
Run Code Online (Sandbox Code Playgroud)
示例输出:
filtered line1
filtered line2
filtered line3
42
Run Code Online (Sandbox Code Playgroud)
注意:子进程从父进程继承打开的文件描述符。这意味着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
Run Code Online (Sandbox Code Playgroud)
在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
Run Code Online (Sandbox Code Playgroud)
自下而上:
#part3) 和右侧 ( #part2) 的命令。exit $xs也是管道的最后一个命令,这意味着来自 stdin 的字符串将是整个构造的退出状态。#part2,反过来将成为整个构造的退出状态。#part5和#part6) 和右侧 ( filter >&4) 的命令。的输出filter被重定向到文件描述符 4。在#part1文件描述符 4 中被重定向到 stdout。这意味着 的输出filter是整个构造的标准输出。#part6打印到文件描述符 3。在#part3文件描述符 3 中被重定向到#part2. 这意味着退出状态 from#part6将是整个构造的最终退出状态。someprog被执行。退出状态为#part5。标准输出由管道接收#part4并转发到filter. 的输出filter将依次到达 stdout,如中所述#part4Chr*_*ris 39
虽然不完全符合您的要求,但您可以使用
#!/bin/bash -o pipefail
Run Code Online (Sandbox Code Playgroud)
以便您的管道返回最后一个非零返回。
可能会少一点编码
编辑:示例
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
Run Code Online (Sandbox Code Playgroud)
Gil*_*il' 22
什么可以当我这样做是从饲料退出代码foo为bar。例如,如果我知道foo永远不会产生只有数字的行,那么我可以添加退出代码:
{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'
Run Code Online (Sandbox Code Playgroud)
或者,如果我知道从foonever的输出只包含一行.:
{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'
Run Code Online (Sandbox Code Playgroud)
如果有某种方法bar可以处理除最后一行之外的所有内容,并将最后一行作为其退出代码传递,则始终可以完成此操作。
如果bar是一个复杂的管道,您不需要其输出,您可以通过在不同的文件描述符上打印退出代码来绕过它的一部分。
exit_codes=$({ { foo; echo foo:"$?" >&3; } |
{ bar >/dev/null; echo bar:"$?" >&3; }
} 3>&1)
Run Code Online (Sandbox Code Playgroud)
在这之后$exit_codes通常是foo:X bar:Y,但它可能是bar:Y foo:X,如果bar退出所有阅读它的输入之前,或者如果你运气不好。我认为写入最多 512 字节的管道在所有 unices 上都是原子的,因此只要标签字符串低于 507 字节,就不会混合foo:$?和bar:$?部分。
如果您需要从 捕获输出bar,则变得困难。您可以通过安排barnever的输出包含看起来像退出代码指示的行来组合上述技术,但它确实变得繁琐。
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')
Run Code Online (Sandbox Code Playgroud)
当然,还有使用临时文件来存储状态的简单选项。简单,但在生产中并不那么简单:
/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")
Run Code Online (Sandbox Code Playgroud)
Jan*_*der 17
从管道开始:
foo | bar | baz
Run Code Online (Sandbox Code Playgroud)
这是仅使用 POSIX shell 而没有临时文件的通用解决方案:
exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
(bar || echo "1:$?" >&3) |
(baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-
Run Code Online (Sandbox Code Playgroud)
$error_statuses 包含任何失败进程的状态代码,以随机顺序排列,并带有指示哪个命令发出每个状态的索引。
# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2
# test if all commands succeeded:
test -z "$error_statuses"
# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null
Run Code Online (Sandbox Code Playgroud)
注意$error_statuses我的测试中的引号;没有它们grep就无法区分,因为换行符被强制为空格。
mtr*_*eur 12
所以我想贡献一个像 lesmana 的答案,但我认为我的可能更简单一点,而且更有利的纯 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.
Run Code Online (Sandbox Code Playgroud)
我认为最好从内向外解释——command1 将在标准输出(文件描述符 1)上执行并打印其常规输出,然后一旦完成,printf 将在其标准输出上执行并打印 command1 的退出代码,但该标准输出被重定向到3. 文件描述符
当 command1 正在运行时,它的 stdout 正在通过管道传输到 command2(printf 的输出永远不会将其发送到 command2,因为我们将它发送到文件描述符 3 而不是 1,这是管道读取的内容)。然后我们将 command2 的输出重定向到文件描述符 4,这样它也不会出现在文件描述符 1 之外——因为我们希望文件描述符 1 稍后空闲,因为我们会将文件描述符 3 上的 printf 输出带回文件描述符1 - 因为这是命令替换(反引号)将捕获的内容,这就是将放入变量的内容。
最后一点神奇的是,首先exec 4>&1我们作为一个单独的命令执行——它打开文件描述符 4 作为外部 shell 标准输出的副本。命令替换将从其中的命令的角度捕获标准输出上写入的任何内容 - 但是,由于 command2 的输出将发送到文件描述符 4 就命令替换而言,命令替换不会捕获它 - 但是,一旦它从命令替换中“退出”,它实际上仍然会转到脚本的整体文件描述符 1。
(exec 4>&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>&-
Run Code Online (Sandbox Code Playgroud)
请注意,我在我的示例中使用了复合命令,但是子 shell(使用( )而不是{ }也可以工作,但可能效率较低。)
命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符 4,后面的复合命令3>&1将继承文件描述符 3。因此4>&-确保内部复合命令不会继承文件描述符 4,并且3>&-不会继承文件描述符 3,因此 command1 得到一个“更干净”、更标准的环境。您也可以将内部移动到4>&-旁边3>&-,但我想为什么不尽可能限制其范围。
我不确定直接使用文件描述符 3 和 4 的频率 - 我认为大多数时候程序使用返回未使用的文件描述符的系统调用,但有时代码直接写入文件描述符 3,我猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有则相应地表现不同)。所以后者可能最好记住并用于通用情况。
小智 7
上面 lesmana 的解决方案也可以通过使用{ .. }代替来完成,而无需启动嵌套子进程的开销(记住这种形式的分组命令总是必须以分号结束)。像这样的东西:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
Run Code Online (Sandbox Code Playgroud)
我已经使用 dash 版本 0.5.5 和 bash 版本 3.2.25 和 4.2.42 检查了这个构造,所以即使某些 shell 不支持{ .. }分组,它仍然符合 POSIX。
这是可移植的,即可以与任何 POSIX 兼容的 shell 一起使用,不需要当前目录可写,并且允许使用相同技巧的多个脚本同时运行。
(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))
Run Code Online (Sandbox Code Playgroud)
编辑:这是根据吉尔斯的评论的更强版本:
(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))
Run Code Online (Sandbox Code Playgroud)
Edit2:这是一个稍轻的变体,遵循 dubiousjim 评论:
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
Run Code Online (Sandbox Code Playgroud)
以下是@Patrik 答案的补充,以防您无法使用其中一种常见解决方案。
这个答案假设如下:
$PIPESTATUS也不知道的壳set -o pipefail额外的假设。你可以去掉所有的,但这对食谱的破坏太大了,所以这里不涉及:
- 您只想知道 PIPE 中的所有命令都具有退出代码 0。
- 您不需要额外的边带信息。
- 您的 shell 会等待所有管道命令返回。
之前:foo | bar | baz,但是这仅返回最后一个命令的退出代码 ( baz)
通缉:$?不能是0(真),如果管道中的任何命令失败
后:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
# $? now is 0 only if all commands had exit code 0
Run Code Online (Sandbox Code Playgroud)
解释:
mktemp. 这通常会立即在/tmpwait是需要ksh的,因为ksh别人不会等待所有管道命令完成。但是请注意,如果存在一些后台任务,则会产生不需要的副作用,因此我默认将其注释掉。如果等待没有受到伤害,您可以评论它。read返回false,所以true表示一个错误这可以用作单个命令的插件替换,只需要以下内容:
/proc/fd/N错误:
此脚本有一个错误,以防/tmp空间不足。如果您也需要针对这种人为情况进行保护,您可以按如下方式进行,但是这样做有一个缺点,即0in000的数量取决于管道中的命令数量,因此稍微复杂一些:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
Run Code Online (Sandbox Code Playgroud)
便携性注意事项:
ksh和只等待最后一个管道命令的类似 shell 需要未wait注释的
最后一个例子使用了printf "%1s" "$?"而不是echo -n "$?"因为它更便携。并非每个平台都能-n正确解释。
printf "$?"也会这样做,但是printf "%1s"如果您在某个真正损坏的平台上运行脚本,则会捕获一些极端情况。(阅读:如果您碰巧在paranoia_mode=extreme.)
FD 8 和 FD 9 在支持多位数的平台上可以更高。AFAIR 符合 POSIX 的 shell 只需要支持个位数。
已用 Debian 8.2 sh、bash、ksh、ash、sash甚至csh
| 归档时间: |
|
| 查看次数: |
198044 次 |
| 最近记录: |