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

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 the command set succeed or not.

Currently I am storing the exit code and testing it numerically, something like this:

long_running_command | grep -q trigger_word
status=$?

if [ $status -eq 0 ]; then
    : stuff
else

: more code

if [ $status -eq 0 ]; then
    : stuff
else

For some reason it feels like this should be simpler. We have a simple exit code stored and now we are repeatedly typing out numerical test operations to run on it. For example I can cheat use the string output instead of the return code which is simpler to test for:

status=$(long_running_command | grep trigger_word)

if [ $status ]; then
    : stuff
else

: more code

if [ $status ]; then
    : stuff
else

On the surface this looks more straight forward, but I realize it's dirty.

If the other logic wasn't so complex and I was only running this once, I realize I could embed it in place of the test operator, but this is not ideal when you need to reuse the results in other locations without re-running the test:

if long_running_command | grep -q trigger_word; then
    : stuff
else

The only thing I've found so far is assigning the code as part of command substitution:

status=$(long_running_command | grep -q trigger_word; echo $?)

if [ $status -eq 0 ]; then
    : stuff
else

Even this is not technically a one shot assignment (although some may argue the readability is better) but the necessary numerical test syntax still seems cumbersome to me. Maybe I'm just being OCD.

Am I missing a more elegant way to assign an exit code to a variable then branch on it later?


Solution 1:

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

Solution 2:

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.