Is there an elegant way to store and evaluate return values in bash scripts?

后端 未结 6 2092
傲寒
傲寒 2020-12-01 20:03

I have a rather complex series of commands in bash that ends up returning a meaningful exit code. Various places later in the script need to branch conditionally on whether

相关标签:
6条回答
  • 2020-12-01 20:05

    Based on the OP's clarification that it's only about success v. failure (as opposed to the specific exit codes):

    long_running_command | grep -q trigger_word || failed=1
    
    if ((!failed)); then
      : stuff
    else
    
    : more code
    
    if ((!failed)); then
      : stuff
    else
    
    • Sets the success-indicator variable only on failure (via ||, i.e, if a non-zero exit code is returned).
    • Relies on the fact that variables that aren't defined evaluate to false in an arithmetic conditional (( ... )).
    • Care must be taken that the variable ($failed, in this example) hasn't accidentally been initialized elsewhere.

    (On a side note, as @nos has already mentioned in a comment, you need to be careful with commands involving a pipeline; from man bash (emphasis mine):

    The return status of a pipeline is the exit status of the last command, unless the pipefail option is enabled. If pipefail is enabled, the pipeline's return status is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands exit successfully.

    To set pipefail (which is OFF by default), use set -o pipefail; to turn it back off, use set +o pipefail.)

    0 讨论(0)
  • 2020-12-01 20:10

    Hmm, the problem is a bit vague - if possible, I suggest considering refactoring/simplify, i.e.

    function check_your_codes {
    # ... run all 'checks' and store the results in an array
    }
    ###
    function process_results {
    # do your 'stuff' based on array values
    }
    ###
    create_My_array
    check_your_codes
    process_results
    

    Also, unless you really need to save the exit code then there is no need to store_and_test - just test_and_do, i.e. use a case statement as suggested above or something like:

    run_some_commands_and_return_EXIT_CODE_FROM_THE_LAST_ONE
    if [[ $? -eq 0 ]] ; then do_stuff else do_other_stuff ; fi
    
    :)
    Dale
    
    0 讨论(0)
  • 2020-12-01 20:13

    If you don't care about the exact error code, you could do:

    if long_running_command | grep -q trigger_word; then
        success=1
        : success
    else
        success=0
        : failure
    fi
    
    if ((success)); then
        : success
    else
        : failure
    fi
    

    Using 0 for false and 1 for true is my preferred way of storing booleans in scripts. if ((flag)) mimics C nicely.

    If you do care about the exit code, then you could do:

    if long_running_command | grep -q trigger_word; then
        status=0
        : success
    else
        status=$?
        : failure
    fi
    
    if ((status == 0)); then
        : success
    else
        : failure
    fi
    

    I prefer an explicit test against 0 rather than using !, which doesn't read right.

    (And yes, $? does yield the correct value here.)

    0 讨论(0)
  • 2020-12-01 20:21

    The simple solution:

    output=$(complex_command)
    status=$?
    
    if (( status == 0 )); then
        : stuff with "$output"
    fi
    
    : more code
    
    if (( status == 0 )); then
        : stuff with "$output"
    fi
    

    Or more eleganter-ish

    do_complex_command () { 
        # side effects: global variables
        # store the output in $g_output and the status in $g_status
        g_output=$(
            command -args | commands | grep -q trigger_word
        )
        g_status=$?
    }
    complex_command_succeeded () {
        test $g_status -eq 0
    }
    complex_command_output () {
        echo "$g_output"
    }
    
    do_complex_command
    
    if complex_command_succeeded; then
        : stuff with "$(complex_command_output)"
    fi
    
    : more code
    
    if complex_command_succeeded; then
        : stuff with "$(complex_command_output)"
    fi
    

    Or

    do_complex_command () { 
        # side effects: global variables
        # store the output in $g_output and the status in $g_status
        g_output=$(
            command -args | commands
        )
        g_status=$?
    }
    complex_command_output () {
        echo "$g_output"
    }
    complex_command_contains_keyword () {
        complex_command_output | grep -q "$1"
    }
    
    if complex_command_contains_keyword "trigger_word"; then
        : stuff with "$(complex_command_output)"
    fi
    
    0 讨论(0)
  • 2020-12-01 20:25

    If you don't need to store the specific exit status, just whether the command succeeded or failed (e.g. whether grep found a match), I's use a fake boolean variable to store the result:

    if long_running_command | grep trigger_word; then
        found_trigger=true
    else
        found_trigger=false
    fi
    
    # ...later...
    if ! $found_trigger; then
        # stuff to do if the trigger word WASN'T found
    fi
    
    #...
    if $found_trigger; then
        # stuff to do if the trigger WAS found
    fi
    

    Notes:

    • The shell doesn't really have boolean (true/false) variables. What's actually happening here is that "true" and "false" are stored as strings in the found_trigger variable; when if $found_trigger; then executes, it runs the value of $found_trigger as a command, and it just happens that the true command always succeeds and the false command always fails, thus causing "the right thing" to happen. In if ! $found_trigger; then, the "!" toggles the success/failure status, effectively acting as a boolean "not".
    • if long_running_command | grep trigger_word; then is equivalent to running the command, then using if [ $? -ne 0 ]; then to check its exit status. I find it a little cleaner, but you have to get used to thinking of if as checking the success/failure of a command, not just testing boolean conditions. If "active" if commands aren't intuitive to you, use a separate test instead.
    • As Charles Duffy pointed out in a comment, this trick executes data as a command, and if you don't have full control over that data... you don't have control over what your script is going to do. So never set a fake-boolean variable to anything other than the fixed strings "true" and "false", and be sure to set the variable before using it. If you have any nontrivial execution flow in the script, set all fake-boolean variables to sane default values (i.e. "true" or "false") before the execution flow gets complicated.

      Failure to follow these rules can lead to security holes large enough to drive a freight train through.

    0 讨论(0)
  • 2020-12-01 20:29

    Why don't you set flags for the stuff that needs to happen later?

    cheeseballs=false
    nachos=false
    guppies=false
    
    command
    case $? in
        42) cheeseballs=true ;;
        17 | 31) cheeseballs=true; nachos=true; guppies=true;;
        66) guppies=true; echo "Bingo!";;
    esac
    
    $cheeseballs && java -crash -burn
    $nachos && python ./tex.py --mex
    if $guppies; then
        aquarium --light=blue --door=hidden --decor=squid
    else
        echo SRY
    fi
    

    As pointed out by @CharlesDuffy in the comments, storing an actual command in a variable is slightly dubious, and vaguely triggers Bash FAQ #50 warnings; the code reads (slightly & IMHO) more naturally like this, but you have to be really careful that you have total control over the variables at all times. If you have the slightest doubt, perhaps just use string values and compare against the expected value at each junction.

    [ "$cheeseballs" = "true" ] && java -crash -burn
    

    etc etc; or you could refactor to some other implementation structure for the booleans (an associative array of options would make sense, but isn't portable to POSIX sh; a PATH-like string is flexible, but perhaps too unstructured).

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