【问题标题】:Checking Bash exit status of several commands efficiently有效检查多个命令的 Bash 退出状态
【发布时间】:2011-07-08 21:53:24
【问题描述】:

对于多个命令,是否存在类似于 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

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

【问题讨论】:

标签: bash exit


【解决方案1】:

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

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

mytest "$command1"
mytest "$command2"

【讨论】:

  • 不要使用$*,如果任何参数中有空格,它将失败;请改用"$@"。同样,将$1 放在echo 命令的引号内。
  • 另外我会避免使用名称test,因为这是一个内置命令。
  • 这是我采用的方法。老实说,我认为我在原始帖子中不够清楚,但是这种方法允许我编写自己的“测试”函数,因此我可以在那里执行我喜欢的错误操作,这些操作与执行的操作相关剧本。谢谢:)
  • 如果出现错误,test() 返回的退出代码不会总是返回 0,因为最后执行的命令是“echo”。您可能需要保存 $?首先。
  • 这不是一个好主意,它会鼓励不良做法。考虑ls 的简单情况。如果您调用ls foo 并收到ls: foo: No such file or directory\n 形式的错误消息,您就会理解问题所在。相反,如果你得到ls: foo: No such file or directory\nerror with ls\n,你就会被多余的信息分心。在这种情况下,很容易认为多余是微不足道的,但它会迅速增长。简洁的错误信息很重要。但更重要的是,这种类型的包装器也鼓励编写者完全忽略好的错误消息。
【解决方案2】:

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

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

【讨论】:

  • 请注意,虽然此解决方案是最简单的,但它不允许您在失败时执行任何清理。
  • 清理可以通过陷阱来完成。 (例如trap some_func 0 将在退出时执行some_func
  • 另外请注意,errexit (set -e) 的语义在不同版本的 bash 中发生了变化,并且在函数调用和其他设置期间经常会出现意外行为。我不再推荐使用它。 IMO,最好在每个命令之后明确写|| exit
【解决方案3】:

我有一组脚本功能,在我的 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 脚本知识,但我认为它仍然是一件艺术品。
  • 这个工具有正式名称吗?我很想阅读有关这种步骤/尝试/下一个日志记录风格的手册页
  • 这些shell函数好像在Ubuntu上不可用?我希望使用这个,虽然是便携的东西
  • @ThorSummoner,这可能是因为 Ubuntu 使用 Upstart 而不是 SysV init,并且很快就会使用 systemd。 RedHat 倾向于长期保持向后兼容性,这就是为什么 init.d 仍然存在的原因。
  • 我发布了对 John 解决方案的扩展,并允许在非 RedHat 系统(如 Ubuntu)上使用它。见stackoverflow.com/a/54190627/308145
【解决方案4】:

对于它的价值,编写代码来检查每个命令是否成功的更短方法是:

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

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

【讨论】:

  • 没想到这一点,不是我采用的方法,但它快速且易于阅读,感谢您提供的信息:)
  • 静默执行命令并达到同样的效果:command1 &amp;&gt; /dev/null || echo "command1 borked it"
  • 我是这种方法的粉丝,有没有办法在 OR 之后执行多个命令?类似command1 || (echo command1 borked it ; exit)
【解决方案5】:

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

command1 &&
  command2 &&
  command3

这不是您在问题中要求的语法,但它是您描述的用例的常见模式。一般来说,这些命令应该负责打印失败,这样您就不必手动执行此操作(可能使用-q 标志来在您不想要错误时消除错误)。如果您有能力修改这些命令,我​​会编辑它们以在失败时大喊大叫,而不是将它们包装在其他可以这样做的东西中。


另请注意,您不需要这样做:

command1
if [ $? -ne 0 ]; then

你可以简单地说:

if ! command1; then

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

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

【讨论】:

    【解决方案6】:

    不要创建运行器函数或使用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

    【讨论】:

    • 如果您想更接近地模仿 try 块,请使用 trap - ERR 在“块”末尾关闭陷阱。
    【解决方案7】:

    我个人更喜欢使用轻量级的方法,如here;

    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】:

      我在 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 boilerplate/framework 的一部分。它进一步扩展了 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

      【讨论】:

      • 我看过 repo 并不会自己使用它,因为这对我来说太神奇了(如果需要更多抽象能力,IMO 最好使用 Python),但绝对很大 +1 来自我,因为它看起来棒极了。
      • 感谢@AlexanderMalakhov 的客气话。我同意“魔法”的数量——这是我们头脑风暴一个简化的 3.0 版本框架的原因之一,这将更容易理解、调试等。关于 GH 的 3.0 有一个未解决的问题,如果你想插播你的想法。
      【解决方案9】:
      run() {
        $*
        if [ $? -ne 0 ]
        then
          echo "$* failed with exit code $?"
          return 1
        else
          return 0
        fi
      }
      
      run command1 && run command2 && run command3
      

      【讨论】:

      • 不要运行$*,如果任何参数中有空格,它将失败;请改用"$@"。 (虽然 $* 在 echo 命令中是可以的。)
      【解决方案10】:

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

      【讨论】:

        【解决方案11】:

        献给偶然发现这个话题的fish shell 用户。

        foo 成为一个不“返回”(回显)值的函数,但它像往常一样设置退出代码。
        为了避免在调用函数后检查$status,您可以这样做:

        foo; and echo success; or echo failure
        

        如果它太长而不能放在一行中:

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

        【讨论】:

          【解决方案12】:

          当我使用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
          

          【讨论】:

            【解决方案13】:

            您可以在非 RedHat 系统上使用上面找到的 @john-kugelman 的 awesome solution,方法是在他的代码中注释掉这一行:

            . /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
            } 
            

            【讨论】:

              【解决方案14】:

              以功能方式检查状态

              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
              

              【讨论】:

                【解决方案15】:

                假设

                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
                

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2010-10-20
                  • 2013-02-10
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-03-25
                  • 2013-02-28
                  • 2014-12-03
                  相关资源
                  最近更新 更多