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


260

是否有类似于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

因为我相信每个命令的参数(如果我写错了,请更正)会互相干扰。这两种方法对我来说似乎是漫长而令人讨厌的,因此我在这里呼吁一种更有效的方法。


2
看一看非官方的bash严格模式set -euo pipefail
Pablo A

1
@PabloBianchi,set -e是一个可怕的想法。请参阅BashFAQ#105中的练习,其中仅讨论了它引入的一些意外边缘情况,和/或比较了in-ulm.de/~mascheck/various/set中不同shell(和shell版本)实现之间的不兼容性。 -e
Charles Duffy

Answers:


274

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

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

mytest "$command1"
mytest "$command2"

32
不要使用$*,如果任何参数中有空格,它将失败。使用"$@"代替。同样,$1echo命令中加引号。
Gordon Davisson

82
另外,我会避免使用该名称,test因为它是内置命令。
John Kugelman

1
这是我使用的方法。老实说,我认为我在原始帖子中不够清楚,但是此方法允许我编写自己的“测试”函数,以便随后可以在其中执行与我在其中执行的操作相关的错误操作。剧本。谢谢:)
jwbensley 2011年

7
由于最后执行的命令是'echo',因此在发生错误的情况下,test()返回的退出代码是否总是返回0。您可能需要保存$的值?第一。
magiconair

2
这不是一个好主意,并且会鼓励不良做法。考虑的简单情况ls。如果您调用ls foo并得到以下形式的错误消息,ls: foo: No such file or directory\n您就可以理解问题所在。如果相反,您会ls: foo: No such file or directory\nerror with ls\n被多余的信息分心。在这种情况下,很容易说出多余是微不足道的,但是它很快就增长了。简洁的错误消息很重要。但更重要的是,这种类型的包装器也鼓励编写者完全忽略良好的错误消息。
威廉·普塞尔

185

“退出并回显错误”是什么意思?如果您想让脚本在任何命令失败后立即终止,则只需执行

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改变了其语义,尽管它对于简单的脚本也可以正常工作,但由于存在许多极端情况,因此根本无法使用。(请考虑以下内容:set -e; foo() { false; echo should not print; } ; foo && echo ok 此处的语义有些合理,但是如果将代码重构为依赖于选项设置以尽早终止的函数,则很容易被咬伤。)IMO最好编写为:

 #!/bin/sh

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

要么

#!/bin/sh

command1 && command2 && command3

1
请注意,虽然此解决方案是最简单的解决方案,但它不允许您对失败进行任何清理。
2015年

6
可以使用陷阱完成清理。(例如trap some_func 0some_func在出口处执行)
William Pursell 2015年

3
还要注意,errexit(set -e)的语义在bash的不同版本中已更改,并且在函数调用和其他设置期间经常会表现出异常。我不再建议使用它。IMO,最好|| exit在每个命令后都写明确的内容。
William Pursell '17

87

我在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
}

这是纯金。虽然我了解如何使用脚本,但我并没有完全掌握每个步骤,但这绝对超出了我的bash脚本知识,但我仍然认为这是一件艺术品。
Kingmilo 2015年

2
这个工具有正式名称吗?我很想阅读有关这种步进/尝试/下一次记录样式的手册页
ThorSummoner 2015年

这些shell函数似乎在Ubuntu上不可用?我希望能利用这一点,一些便携式十岁上下,虽然
ThorSummoner

@ThorSummoner,这可能是因为Ubuntu使用Upstart而不是SysV init,并将很快使用systemd。RedHat倾向于长期保持向后兼容性,这就是为什么init.d内容仍然存在的原因。
dragon788 '16

我已经发布了John解决方案的扩展,并允许其在非RedHat系统(例如Ubuntu)上使用。参见 stackoverflow.com/a/54190627/308145
Mark Thomson,

51

值得一提的是,编写代码来检查每个命令是否成功的简短方法是:

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

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


没想到这一点,不是我使用的方法,但是它很容易阅读,感谢您提供的信息:)
jwbensley 2011年

3
要以静默方式执行命令并完成相同的操作:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne 2014年

我是这种方法的粉丝,有没有一种方法可以在“或”之后执行多个命令?像command1 || (echo command1 borked it ; exit)
AndreasKralj '18

38

一种替代方法是简单地将命令与一起加入,以&&使第一个失败的命令阻止其余命令执行:

command1 &&
  command2 &&
  command3

这不是您在问题中要求的语法,但这是您描述的用例的常见模式。通常,命令应负责打印失败,这样您就不必手动进行打印(可能在不需要的时候带有-q标记以使错误静音)。如果您有能力修改这些命令,那么我将对其进行编辑以在失败时大喊大叫,而不是将它们包装在其他可以这样做的地方。


另请注意,您无需执行以下操作:

command1
if [ $? -ne 0 ]; then

您可以简单地说:

if ! command1; then

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

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

34

代替创建运行函数或使用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

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


4
如果您想更紧密地模仿try块trap - ERR,请在“块”末尾关闭陷阱。
Gordon Davisson

14

我个人更喜欢使用轻量级的方法,因为看到这里 ;

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"

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

run command1 && run command2 && run command3

6
不要运行$*,如果任何参数中有空格,它将失败。使用"$@"代替。(尽管echo命令中可以使用$ * 。)
Gordon Davisson

6

我在bash中开发了一种几乎完美的try&catch实现,使您可以编写如下代码:

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&catch的代码:

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上


1
我看过repo并不想自己使用它,因为这对我来说太不可思议了(IMO,如果需要更多的抽象能力,最好使用Python),但绝对对我来说是+1,因为它看起来很棒。
亚历山大·马拉霍夫

感谢@AlexanderMalakhov的客气话。我同意“魔术”的数量-这是我们正在集思广益简化框架3.0版本的原因之一,该框架将更易于理解,调试等。GH上存在一个关于3.0的开放性问题,如果您想插入自己的想法。
niieani

3

抱歉,我无法对第一个答案做出评论,但是您应该使用新实例来执行命令: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

2

对于鱼壳谁在这个线程绊倒用户。

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

foo; and echo success; or echo failure

如果太长而无法放在一行上:

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

1

使用时,ssh我需要区分连接问题和errexitset -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

1

您可以在非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
} 

0

以功能方式检查状态

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.