有效地检查几个命令的Bash退出状态

对于多个命令,是否有类似于pipefail的东西,比如'try'语句,但在bash中。我想这样做:

echo "trying stuff"
try {
command1
command2
command3
}

在任何时候,如果任何命令失败了,就退出并回显该命令的错误。我不想做这样的事情:

command1
if [ $? -ne 0 ]; then
echo "command1 borked it"
fi


command2
if [ $? -ne 0 ]; then
echo "command2 borked it"
fi

等等……或者类似的事情:

pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3

因为我相信每个命令的参数(如果我错了请纠正我)会相互干扰。这两种方法对我来说似乎非常冗长和讨厌,所以我在这里呼吁一种更有效的方法。

415349 次浏览
run() {
$*
if [ $? -ne 0 ]
then
echo "$* failed with exit code $?"
return 1
else
return 0
fi
}


run command1 && run command2 && run command3

您可以编写一个函数来启动和测试该命令。假设command1command2是已设置为命令的环境变量。

function mytest {
"$@"
local status=$?
if (( status != 0 )); then
echo "error with $1" >&2
fi
return $status
}


mytest "$command1"
mytest "$command2"

你说的“退出并回复错误”是什么意思?如果您的意思是希望脚本在任何命令失败时立即终止,那么就这样做

set -e    # DON'T do this.  See commentary below.

在脚本的开头(但注意下面的警告)。不要回显错误消息:让失败的命令处理它。换句话说,如果你这样做:

#!/bin/sh


set -e    # Use caution.  eg, don't do this
command1
command2
command3

而command2失败,同时打印一个错误消息到stderr,那么似乎您已经实现了您想要的。(除非我误解了你想要的!)

因此,您编写的任何命令都必须表现良好:它必须向stderr而不是stdout报告错误(问题中的示例代码将错误输出到stdout),并且在失败时必须以非零状态退出。

然而,我不再认为这是一个好的做法。set -e已经用不同版本的bash改变了它的语义,尽管它对于一个简单的脚本工作得很好,但有太多的边缘情况,它本质上是不可用的。这里的语义是合理的,但如果你将代码重构成一个依赖于选项设置提前终止的函数,你很容易被咬。)在我看来,最好这样写:

 #!/bin/sh


command1 || exit
command2 || exit
command3 || exit

#!/bin/sh


command1 && command2 && command3

与其创建运行器函数或使用set -e,不如使用trap:

trap 'echo "error"; do_cleanup failed; exit' ERR
trap 'echo "received signal to stop"; do_cleanup interrupted; exit' SIGQUIT SIGTERM SIGINT


do_cleanup () { rm tempfile; echo "$1 $(date)" >> script_log; }


command1
command2
command3

该trap甚至可以访问触发它的命令的行号和命令行。变量是$BASH_LINENO$BASH_COMMAND

我有一组在Red Hat系统上广泛使用的脚本函数。它们使用来自/etc/init.d/functions的系统函数打印绿色的[ OK ]和红色的[FAILED]状态指示器。

如果你想记录哪些命令失败,你可以选择将$LOG_STEPS变量设置为日志文件名。

使用

step "Installing XFS filesystem tools:"
try rpm -i xfsprogs-*.rpm
next


step "Configuring udev:"
try cp *.rules /etc/udev/rules.d
try udevtrigger
next


step "Adding rc.postsysinit hook:"
try cp rc.postsysinit /etc/rc.d/
try ln -s rc.d/rc.postsysinit /etc/rc.postsysinit
try echo $'\nexec /etc/rc.postsysinit' >> /etc/rc.sysinit
next

输出

Installing XFS filesystem tools:        [  OK  ]
Configuring udev:                       [FAILED]
Adding rc.postsysinit hook:             [  OK  ]

代码

#!/bin/bash


. /etc/init.d/functions


# Use step(), try(), and next() to perform a series of commands and print
# [  OK  ] or [FAILED] at the end. The step as a whole fails if any individual
# command fails.
#
# Example:
#     step "Remounting / and /boot as read-write:"
#     try mount -o remount,rw /
#     try mount -o remount,rw /boot
#     next
step() {
echo -n "$@"


STEP_OK=0
[[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
}


try() {
# Check for `-b' argument to run command in the background.
local BG=


[[ $1 == -b ]] && { BG=1; shift; }
[[ $1 == -- ]] && {       shift; }


# Run the command.
if [[ -z $BG ]]; then
"$@"
else
"$@" &
fi


# Check if command failed and update $STEP_OK if so.
local EXIT_CODE=$?


if [[ $EXIT_CODE -ne 0 ]]; then
STEP_OK=$EXIT_CODE
[[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$


if [[ -n $LOG_STEPS ]]; then
local FILE=$(readlink -m "${BASH_SOURCE[1]}")
local LINE=${BASH_LINENO[0]}


echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS"
fi
fi


return $EXIT_CODE
}


next() {
[[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; }
[[ $STEP_OK -eq 0 ]]  && echo_success || echo_failure
echo


return $STEP_OK
}

不管怎样,编写代码来检查每个命令是否成功的更简单的方法是:

command1 || echo "command1 borked it"
command2 || echo "command2 borked it"

它仍然很乏味,但至少是可读的。

抱歉,我不能对第一个答案作出评论 但是您应该使用新实例来执行命令:cmd_output=$($@)

#!/bin/bash


function check_exit {
cmd_output=$($@)
local status=$?
echo $status
if [ $status -ne 0 ]; then
echo "error with $1" >&2
fi
return $status
}


function run_command() {
exit 1
}


check_exit run_command

就我个人而言,我更喜欢使用轻量级的方法,如在这里;

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }

使用示例:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"

我已经开发了一个几乎完美无缺的尝试。Catch在bash中的实现,允许您编写如下代码:

try
echo 'Hello'
false
echo 'This will not be displayed'


catch
echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

您甚至可以将try-catch块嵌套在它们自己内部!

try {
echo 'Hello'


try {
echo 'Nested Hello'
false
echo 'This will not execute'
} catch {
echo "Nested Caught (@ $__EXCEPTION_LINE__)"
}


false
echo 'This will not execute too'


} catch {
echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
}

该代码是bash样板/框架的一部分。它进一步扩展了try &Catch使用回溯和异常的错误处理(以及其他一些不错的功能)。

下面的代码只负责try &问题:

set -o pipefail
shopt -s expand_aliases
declare -ig __oo__insideTryCatch=0


# if try-catch is nested, then set +e before so the parent handler doesn't catch us
alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e;
__oo__insideTryCatch+=1; ( set -e;
trap \"Exception.Capture \${LINENO}; \" ERR;"
alias catch=" ); Exception.Extract \$? || "


Exception.Capture() {
local script="${BASH_SOURCE[1]#./}"


if [[ ! -f /tmp/stored_exception_source ]]; then
echo "$script" > /tmp/stored_exception_source
fi
if [[ ! -f /tmp/stored_exception_line ]]; then
echo "$1" > /tmp/stored_exception_line
fi
return 0
}


Exception.Extract() {
if [[ $__oo__insideTryCatch -gt 1 ]]
then
set -e
fi


__oo__insideTryCatch+=-1


__EXCEPTION_CATCH__=( $(Exception.GetLastException) )


local retVal=$1
if [[ $retVal -gt 0 ]]
then
# BACKWARDS COMPATIBILE WAY:
# export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
# export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
return 1 # so that we may continue with a "catch"
fi
}


Exception.GetLastException() {
if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
then
cat /tmp/stored_exception
cat /tmp/stored_exception_line
cat /tmp/stored_exception_source
else
echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}"
fi


rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
return 0
}

你可以自由地使用、分叉和贡献——它在GitHub上。

另一种方法是简单地将命令与&&连接在一起,以便第一个失败的命令阻止其余命令的执行:

command1 &&
command2 &&
command3

这不是您在问题中要求的语法,但它是您所描述的用例的通用模式。一般来说,命令应该负责打印失败,这样你就不必手动这样做了(可能会在你不想要错误时使用-q标记来屏蔽错误)。如果您有修改这些命令的能力,我会将它们编辑为在失败时大喊,而不是将它们包装在具有这种功能的其他东西中。


还要注意,你不需要这样做:

command1
if [ $? -ne 0 ]; then

你可以简单地说:

if ! command1; then

当你需要检查返回码时,使用算术上下文而不是[ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then

对于fish shell用户谁偶然发现这个线程。

foo是一个不“返回”(回显)值的函数,但它照常设置退出码 为了避免在调用函数后检查$status,您可以执行:

foo; and echo success; or echo failure

如果它太长,不能放在一行上:

foo; and begin
echo success
end; or begin
echo failure
end

以功能的方式检查状态

assert_exit_status() {


lambda() {
local val_fd=$(echo $@ | tr -d ' ' | cut -d':' -f2)
local arg=$1
shift
shift
local cmd=$(echo $@ | xargs -E ':')
local val=$(cat $val_fd)
eval $arg=$val
eval $cmd
}


local lambda=$1
shift


eval $@
local ret=$?
$lambda : <(echo $ret)


}

用法:

assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls

输出

Status is 127

当我使用ssh时,我需要区分由连接问题引起的问题和errexit (set -e)模式下远程命令的错误代码。我使用以下函数:

# prepare environment on calling site:


rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip"


function exit255 {
local flags=$-
set +e
"$@"
local status=$?
set -$flags
if [[ $status == 255 ]]
then
exit 255
else
return $status
fi
}
export -f exit255


# callee:


set -e
set -o pipefail


[[ $rssh ]]
[[ $remote_ip ]]
[[ $( type -t exit255 ) == "function" ]]


rjournaldir="/var/log/journal"
if exit255 $rssh "[[ ! -d '$rjournaldir/' ]]"
then
$rssh "mkdir '$rjournaldir/'"
fi
rconf="/etc/systemd/journald.conf"
if [[ $( $rssh "grep '#Storage=auto' '$rconf'" ) ]]
then
$rssh "sed -i 's/#Storage=auto/Storage=persistent/' '$rconf'"
fi
$rssh systemctl reenable systemd-journald.service
$rssh systemctl is-enabled systemd-journald.service
$rssh systemctl restart systemd-journald.service
sleep 1
$rssh systemctl status systemd-journald.service
$rssh systemctl is-active systemd-journald.service

你可以在非redhat系统上使用@john-kugelman的很棒的解决方案,注释掉他代码中的这一行:

. /etc/init.d/functions

然后,在最后粘贴下面的代码。完全披露:这只是一个直接的拷贝&粘贴相关位的上述文件采取从Centos 7。

在MacOS和Ubuntu 18.04上测试。


BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"


echo_success() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
echo -n $"  OK  "
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 0
}


echo_failure() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
echo -n $"FAILED"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}


echo_passed() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
echo -n $"PASSED"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}


echo_warning() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
echo -n $"WARNING"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}

假设

alias command1='grep a <<<abc'
alias command2='grep x <<<abc'
alias command3='grep c <<<abc'

要么

{ command1 1>/dev/null || { echo "cmd1 fail"; /bin/false; } } && echo "cmd1 succeed" &&
{ command2 1>/dev/null || { echo "cmd2 fail"; /bin/false; } } && echo "cmd2 succeed" &&
{ command3 1>/dev/null || { echo "cmd3 fail"; /bin/false; } } && echo "cmd3 succeed"

{ { command1 1>/dev/null && echo "cmd1 succeed"; } || { echo "cmd1 fail"; /bin/false; } } &&
{ { command2 1>/dev/null && echo "cmd2 succeed"; } || { echo "cmd2 fail"; /bin/false; } } &&
{ { command3 1>/dev/null && echo "cmd3 succeed"; } || { echo "cmd3 fail"; /bin/false; } }

收益率

cmd1 succeed
cmd2 fail

这是乏味的。但是可读性还不错。