Answers:
如果使用bash
,则可以使用PIPESTATUS
array变量获取管道中每个元素的退出状态。
$ 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
或中运行以上操作,zsh
您将获得相同的结果;只会设置retval_bash
和之一retval_zsh
。另一个将为空白。这将使函数以return $retval_bash $retval_zsh
(以引号引起!)结尾。
pipestatus
在zsh中。不幸的是,其他外壳程序没有此功能。
echo "$pipestatus[1]" "$pipestatus[2]"
。
if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
有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
您可以使用第3个命令示例来获取所需管道中的特定值。
这是解决方案中最笨拙的。分别运行每个命令并捕获状态
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
ksh
,但是从它的联机帮助页面看一眼,它不支持$PIPESTATUS
或类似功能。它确实支持该pipefail
选项。
LOG=$(failed_command | successful_command)
此解决方案无需使用bash特定功能或临时文件即可工作。奖励:最后,退出状态实际上是退出状态,而不是文件中的某些字符串。
情况:
someprog | filter
您需要退出状态someprog
和的输出filter
。
这是我的解决方案:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
此构造的结果从stdout filter
作为构造的stdout ,退出状态从someprog
作为构造的退出状态。
此构造也可用于简单的命令分组{...}
而不是子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
在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
从下至上:
#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,如#part4
(read; exit $REPLY)
(exec 3>&- 4>&-; someprog)
简化为someprog 3>&- 4>&-
。
{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
虽然不完全是您的要求,但您可以使用
#!/bin/bash -o pipefail
以便您的管道返回最后的非零收益。
可能少一些编码
编辑:示例
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
set -o pipefail
脚本中的脚本应该更健壮,例如,如果有人通过执行脚本bash foo.sh
。
-o pipefail
不在POSIX中。
#!/bin/bash -o pipefail
。错误是:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
#!
以外的第一个线,所以这成为/bin/bash
-o pipefail
/tmp/ff
,而不是必要的/bin/bash
-o
pipefail
/tmp/ff
- getopt
(或类似)的解析使用optarg
,这是在下一项目ARGV
,作为参数到-o
,因此失败。如果您要做一个包装器(例如,bash-pf
这样做exec /bin/bash -o pipefail "$@"
就#!
行了,然后把它放到网上,那将起作用。另请参见:en.wikipedia.org/wiki/Shebang_%28Unix%29
如果可能的话,我要做的是将退出代码从中foo
输入bar
。例如,如果我知道foo
永远不会产生仅包含数字的行,那么我可以添加退出代码:
{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'
或者如果我知道输出foo
从不包含带有just的行.
:
{ 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
,但它可能是bar:Y foo:X
,如果bar
阅读所有输入之前,或者如果你运气不好退出。我认为在所有unices上写入最多512个字节的管道都是原子的,因此只要标签字符串在507个字节以下,就不会将foo:$?
和bar:$?
部分混合在一起。
如果您需要从中捕获输出bar
,则将变得很困难。您可以通过安排输出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,但在当今的所有严重的Uniice上都可用。foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")
从管道开始:
foo | bar | baz
这是仅使用POSIX shell而没有临时文件的常规解决方案:
exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
(bar || echo "1:$?" >&3) |
(baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-
$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
注意$error_statuses
测试中的引号;没有它们grep
就无法区分,因为换行符被强制为空格。
所以我想提供一个像莱斯曼纳的答案,但是我认为我的也许是一个更简单,更有利的纯伯恩壳解决方案:
# 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上打印command1的退出代码,但是该stdout重定向到文件描述符3。
当command1运行时,其stdout将通过管道传递给command2(printf的输出从不将其传递给command2,因为我们将其发送到文件描述符3而不是管道读取的1)。然后,我们将command2的输出重定向到文件描述符4,以便它也不会出现在文件描述符1中–因为我们希望稍后释放文件描述符1,因为我们会将文件描述符3的printf输出放回到文件描述符中1 –因为这是命令替换(反引号)将捕获的内容,因此将其放入变量中。
魔术的最后一点是,首先exec 4>&1
我们作为一个单独的命令完成了–它打开文件描述符4作为外部外壳的stdout的副本。从命令内部的角度来看,命令替换将捕获标准上写的所有内容–但是,由于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>&-
请注意,我在示例中使用了复合命令,但是使用了子shell(使用( )
代替{ }
也会起作用,尽管可能效率较低。)
命令从启动它们的进程中继承文件描述符,因此整行第二行将继承文件描述符4,随后的复合命令3>&1
将继承文件描述符3。因此,请4>&-
确保内部复合命令不会继承文件描述符四个,并且3>&-
不会继承文件描述符三个,以便command1获得一个“更干净”的更标准的环境。您也可以将内部移动到4>&-
旁边3>&-
,但我认为为什么不尽可能限制其范围。
我不确定事情多久直接使用文件描述符3和4 –我认为大多数时候程序使用syscall来返回当前未使用的文件描述符,但是有时代码会直接写入文件描述符3猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有打开则相应地表现不同)。因此,可能最好记住后者,并在通用情况下使用。
-bash: 3: Bad file descriptor
。
如果您安装了moreutils软件包,则可以使用mispipe实用程序,它完全可以满足您的要求。
lesmana的上述解决方案也可以实现,而无需使用{ .. }
代替启动嵌套子进程的开销(请记住,这种形式的分组命令总是必须以分号结尾)。像这样:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
我已经用破折号版本0.5.5和bash版本3.2.25和4.2.42检查了此结构,因此,即使某些shell不支持{ .. }
分组,它仍然兼容POSIX。
set -o pipefail
ksh或任意数量的分散命令,我也无法使其与AT&T Ksh(93s +,93u +)或zsh(4.3.9,5.2)一起wait
使用。我认为,至少在某种程度上,这可能是ksh的解析问题,好像我坚持使用subshell一样,它仍然可以正常工作,但是即使if
选择了ksh的subshell变体,但将复合命令留给其他人使用,它也会失败。
这是可移植的,即可以与任何POSIX兼容的外壳一起使用,不需要当前目录可写,并且允许使用同一技巧的多个脚本同时运行。
(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))
编辑:这是继吉尔斯的评论之后的更强版本:
(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))
Edit2:以下是在dubiousjim评论之后的一个稍微轻一些的变体:
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
。@Johan:我同意使用Bash会更容易,但是在某些情况下,知道如何避免Bash是值得的。
如果您无法使用一种常见的解决方案,则以下内容是@Patrik答案的附加内容。
该答案假定如下:
$PIPESTATUS
也不知道的外壳set -o pipefail
其他假设。您可以摆脱所有的麻烦,但是这会使食谱变得过多,因此这里不介绍它:
- 您只想知道PIPE中的所有命令的退出代码均为0。
- 您不需要其他边带信息。
- 您的Shell确实等待所有管道命令返回。
之前:foo | bar | baz
,但这仅返回最后一个命令(baz
)的退出代码
想要的:如果管道中的任何命令失败,则$?
不得为0
(true)
后:
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
解释:
mktemp
。通常这会立即在/tmp
wait
是需要ksh
的,因为ksh
别人不会等待所有管道命令来完成。但是请注意,如果存在某些后台任务,则会有不希望的副作用,因此默认情况下我将其注释掉。如果等待没有影响,可以对其进行注释。read
返回false
,因此true
表示错误可以将其用作单个命令的插件替换,仅需要满足以下条件:
/proc/fd/N
错误:
如果/tmp
空间不足,此脚本有一个错误。如果您也需要保护以防这种人为的情况,则可以按以下步骤进行操作,但是这样做的缺点是,0
in 000
的数量取决于管道中的命令数量,因此稍微复杂一些:
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"
可移植性注意事项:
ksh
而仅等待最后一个管道命令的类似shell需要未wait
注释
最后一个示例使用printf "%1s" "$?"
代替,echo -n "$?"
因为它更易于移植。并非每个平台都能-n
正确解释。
printf "$?"
也可以做到这一点,但是printf "%1s"
如果您在某些真正损坏的平台上运行脚本,则会遇到一些极端情况。(阅读:如果您碰巧在中编程paranoia_mode=extreme
。)
在支持多位数的平台上,FD 8和FD 9可能更高。符合POSIX要求的外壳程序仅需要支持一位数字。
与Debian的8.2测试sh
,bash
,ksh
,ash
,sash
甚至csh
采取一些预防措施,这应该起作用:
foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
以下“ if”块仅在“ command”成功执行后才会运行:
if command; then
# ...
fi
具体来说,您可以运行以下内容:
haconf_out=/path/to/some/temporary/file
if haconf -makerw > "$haconf_out" 2>&1; then
grep -iq "Cluster already writable" "$haconf_out"
# ...
fi
它将运行haconf -makerw
并将其stdout和stderr存储到“ $ haconf_out”。如果from的返回值为haconf
true,则将执行'if'块并将其grep
读取为“ $ haconf_out”,以使其与“ Cluster已经可写”相匹配。
注意管道会自动清理。使用重定向时,您必须小心删除“ $ haconf_out”。
不如优雅pipefail
,但如果无法实现此功能,则是一个合理的选择。
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"
exec 8>&- 9>&-
{
{
{
{ #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8 # use exactly 1x prior to #END
#END
} 2>&1 | ${TEE} 1>&9
} 8>&1
} | exit $(read; printf "${REPLY}")
} 9>&1
exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
编辑:这个答案是错误的,但是很有趣,所以我将其留作以后参考。
!
在命令中
添加a 将反转返回代码。
http://tldp.org/LDP/abs/html/exit-status.html
# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command # bash: bogus_command: command not found
echo $? # 127
! ls | bogus_command # bash: bogus_command: command not found
echo $? # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #
ls
,而不要反转bogus_command