How do I get the effect and usefulness of “set -e” inside a shell function?

后端 未结 10 1520
北荒
北荒 2020-11-28 06:18

set -e (or a script starting with #!/bin/sh -e) is extremely useful to automatically bomb out if there is a problem. It saves me having to error ch

相关标签:
10条回答
  • 2020-11-28 06:51

    You may directly use a subshell as your function definition and set it to exit immediately with set -e. This would limit the scope of set -e to the function subshell only and would later avoid switching between set +e and set -e.

    In addition, you can use a variable assignment in the if test and then echo the result in an additional else statement.

    # use subshell for function definition
    f() (
       set -exo pipefail
       echo a
       false
       echo Should NOT get HERE
       exit 0
    )
    
    # next line also works for non-subshell function given by agsamek above
    #if ret="$( set -e && f )" ; then 
    if ret="$( f )" ; then
       true
    else
       echo "$ret"
    fi
    
    # prints
    # ++ echo a
    # ++ false
    # a
    
    0 讨论(0)
  • 2020-11-28 06:51

    You will need to call your function in a sub shell (inside brackets ()) to achieve this.

    I think you want to write your script like this:

    #!/bin/sh -e
    
    my_function() {
        echo "the following command could fail:"
        false
        echo "this is after the command that fails"
    }
    
    (my_function)
    
    if [ $? -ne 0 ] ; then
        echo "dealing with the problem"
    fi
    
    echo "run this all the time regardless of the success of my_function"
    

    Then the output is (as desired):

    the following command could fail:
    dealing with the problem
    run this all the time regardless of the success of my_function
    
    0 讨论(0)
  • 2020-11-28 06:52

    From documentation of set -e:

    When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value > 0, and is not part of the compound list following a while, until, or if keyword, and is not a part of an AND or OR list, and is not a pipeline preceded by the ! reserved word, then the shell shall immediately exit.

    In your case, false is a part of a pipeline preceded by ! and a part of if. So the solution is to rewrite your code so that it isn't.

    In other words, there's nothing special about functions here. Try:

    set -e
    ! { false; echo hi; }
    
    0 讨论(0)
  • 2020-11-28 06:57

    Note/Edit: As a commenter pointed out, this answer uses bash, and not sh like the OP used in his question. I missed that detail when I originaly posted an answer. I will leave this answer up anyway since it might be interested to some passerby.

    Y'aaaaaaaaaaaaaaaaaaallll ready for this?

    Here's a way to do it with leveraging the DEBUG trap, which runs before each command, and sort of makes errors like the whole exception/try/catch idioms from other languages. Take a look. I've made your example one more 'call' deep.

    #!/bin/bash
    
    # Get rid of that disgusting set -e.  We don't need it anymore!
    # functrace allows RETURN and DEBUG traps to be inherited by each
    # subshell and function.  Plus, it doesn't suffer from that horrible
    # erasure problem that -e and -E suffer from when the command 
    # is used in a conditional expression.
    set -o functrace
    
    # A trap to bubble up the error unless our magic command is encountered 
    # ('catch=$?' in this case) at which point it stops.  Also don't try to
    # bubble the error if were not in a function.
    trap '{ 
        code=$?
        if [[ $code != 0 ]] && [[ $BASH_COMMAND != '\''catch=$?'\'' ]]; then
            # If were in a function, return, else exit.
            [[ $FUNCNAME ]] && return $code || exit $code
        fi
    }' DEBUG
    
    my_function() {
        my_function2
    }
    
    my_function2() {
        echo "the following command could fail:"
        false
        echo "this is after the command that fails"
    }
    
    # the || isn't necessary, but the 'catch=$?' is.
    my_function || catch=$?
    echo "Dealing with the problem with errcode=$catch (⌐■_■)"
    
    echo "run this all the time regardless of the success of my_function"
    

    and the output:

    the following command could fail:
    Dealing with the problem with errcode=1 (⌐■_■)
    run this all the time regardless of the success of my_function
    

    I haven't tested this in the wild, but off the top of my head, there are a bunch of pros:

    1. It's actually not that slow. I've ran the script in a tight loop with and without the functrace option, and times are very close to each other under 10 000 iterations.

    2. You could expand on this DEBUG trap to print a stack trace without doing that whole looping over $FUNCNAME and $BASH_LINENO nonsense. You kinda get it for free (besides actually doing an echo line).

    3. Don't have to worry about that shopt -s inherit_errexit gotcha.

    0 讨论(0)
提交回复
热议问题