7

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?

Caleb
  • 5,084
  • 1
  • 46
  • 65
  • 5
    It feels like `status=$(commands; echo $?)` is worse than `commands; status=$?`, doesn't it? I mean you pipe your exit code into stdout and assign the output to a variable. Isn't it more 'clean' to assign it directly, like you did in the first example? – MrPaulch Jan 23 '14 at 20:00
  • 1
    @MrPaulch That's subjective, but either way has issues. The command substitution is sometimes eaasier to read because you get an idea what the outcome is going to be up front. The direct way can be confusing in the "wait what commands code is this catching again" sort of way. But I'm not trying to vouch for it, I want to know if there is something more elegant that either of these. I was just showing the "what have you tried so far" that makes for a more answerable question. – Caleb Jan 23 '14 at 20:04
  • @Caleb: Agreed re readability, but the difference is more than a matter of style: the command-substitution approach is less efficient, because a subshell - another process - is needlessly created (though IRL it may not matter much). – mklement0 Jan 23 '14 at 20:10
  • 1
    Well i think i just got used to the fact, that in bash you catch the exit code immediatly after the command. So don't have an issues with it. I was pointing out, that by attempting to enhance your formal design approach you actually worsend the logical integrity of the script. (Namly: See mklement0's post) To be constructive: I guess a more elegant way would be to put your commands in a function that returns their exit code... – MrPaulch Jan 23 '14 at 20:10
  • @MrPaulch I realize about the subshell: note that isn't the example that I labeled as one that I was currently doing, only a syntax option I found so that isn't the answer I'm looking for. Is for using a function, that would incur even more overhead. If I put the test commands in a function that returns a code, the whole test suite would be run every time I branched on this. – Caleb Jan 23 '14 at 20:15
  • If your test suit does not have to be run every time then i think assigning it to a well named variable is the most prudent course, but then one line of assigning is not much of an overhead. It would be if you run your test suit many times, then you could use a function...or the & operator for that matter. Sorry, but i don't see a problem (not even in regards of elegance) Keep in mind that bash is not a programming language. But maybe i don't sufficiantly get where you're going with this. – MrPaulch Jan 23 '14 at 20:23
  • 2
    Your first example of just doing `status=$?` is perfectly reasonable and simple. Just note that the exit status of a pipeline can be somewhat convoluted. Read e.g. the manpage of bash and look for the `pipefail` option (as in `set -o pipefail` ) – nos Jan 23 '14 at 20:24
  • @MrPaulch You seem stuck on the assignment end. Really the test end is where it seems cumbersome. Normally if you need it once a bash `if command` is a rather elegant way to test an exit code. As soon as you want to reuse the results you suddenly have to assign the code to a varable in two steps and break out hand coded numerical operators every time you want to check for it, e.g. `if [ $exit_code -eq 0 ]`. It just seems like there ought to me a more streamlined way of doing the repeated testing when is is so easy to do once. – Caleb Jan 23 '14 at 20:34
  • 2
    Well if you worry about the testing try: `if (( $exit_code ));` or add a ! depending on how you define success – MrPaulch Jan 23 '14 at 21:09
  • @MrPaulch: You can even shave off 1 additional char. :) `if (( exit_code ))` works too. – mklement0 Jan 23 '14 at 21:54
  • One clarification might help: do you just care about success v. failure, or does the specific exit code matter? – mklement0 Jan 23 '14 at 21:55
  • @mklement0 Success vs failure is fine, the exact exit code is not important to me. If it was I wouldn't be so bothered by doing numerical tests, – Caleb Jan 23 '14 at 22:01
  • @mklement0 Re "_the command-substitution approach is less efficient, because a subshell - another process - is needlessly created_", I think this is incorrect. Most command substitutions in bash do NOT result in a subshell process being spawned. I'm not sure if this is a special case, but I'm not sure why it would be. It is evaluated and if necessary sometimes a subshell is used, but most operations do not warrant one. – Caleb Jan 23 '14 at 22:11
  • @Caleb: Interesting; I'd be curious to know when subshells are and aren't created - do you have a specific example of a case where a subshell is NOT created? – mklement0 Jan 23 '14 at 22:24
  • @mklement0 I have posed a separate question about command substitution and subshells, perhaps you would like to contribute your findings there or just wait to learn something new: [When exactly will using command substitution spawn more subshells than not doing so?](http://stackoverflow.com/q/21331042) – Caleb Jan 24 '14 at 11:07

6 Answers6

3

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
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • Otherwise, a good list of conditional evaluation patterns. I would add `case $mySpecificStat in 0 ) echo "OK" ;; 1) echo "Nope!" ;; * ) echo "what the?!" ;; esac`. Good luck to all – shellter Jan 23 '14 at 21:29
3

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.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • 2
    That's an unusual way of storing a boolean value. I'm not sure if I like it or loathe it...! – John Kugelman Jan 23 '14 at 21:45
  • @JohnKugelman: I know what you mean; it reminds me of [Duff's Device](http://en.wikipedia.org/wiki/Duff's_device) in that it's simultaneously elegant and *wrong*. – Gordon Davisson Jan 24 '14 at 02:41
  • 1
    I'm thoroughly on the "loathe" side -- you're running a variable's value *as a command*, which means that if an untrusted source can make your code assign their value to that variable, they can run any code that doesn't fall afoul of BashFAQ #50. If there's a codepath that doesn't initialize that variable at all, then such a command can be injected by anything that can set environment variables during your command's execution -- look at the various routes to ShellShock as examples of how that can be done. – Charles Duffy Aug 31 '18 at 19:47
  • @CharlesDuffy Yep, you gotta pinky-swear never leave a "boolean" variable unset, or set it to anything but the fixed strings "true" and "false" (especially nothing dynamically generated). IMO the risk of an unexpected execution path leaving it uninitialized is the scarier problem. If you have any nontrivial execution flow in the script, you should set a default value at the beginning of the script, before things get complicated. I should probably edit my answer to make this clearer. – Gordon Davisson Aug 31 '18 at 22:27
3

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).

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • This is basically the same solution as [Gordon's previous answer](http://stackoverflow.com/a/21319932/313192) except he explains WHY it works and the fact that this is not actually testing a variable but running a command. – Caleb Jan 23 '14 at 22:15
  • If you only care about success vs failure, his is all you need. I wanted to illustrate how easily it stretches in various directions, and hope that this will be helpful for future visitors who want to solve the problem as I originally interpreted it. – tripleee Jan 23 '14 at 22:27
  • With my security hat on, I don't trust this practice at all; see [comment on Gordon's answer](https://stackoverflow.com/questions/21317997/is-there-an-elegant-way-to-store-and-evaluate-return-values-in-bash-scripts#comment91194941_21319932). – Charles Duffy Aug 31 '18 at 19:50
2

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.)

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    You have to be careful with this approach, because you're using the (standardish) convention that 0=false and 1-true, while bash exit statuses follow the opposite convention (see [here](http://stackoverflow.com/questions/11714341/c-program-return-codes-and-bash-symbol)). For example, `((0)); echo $?` prints "1", while `((0)); echo $?` prints "0". If you lose track of which convention is being used where, you'll get the logic backward. – Gordon Davisson Jan 24 '14 at 02:49
  • @GordonDavisson: I see your point, but I don't think this will be a problem in practice: (a) By itself, the meaning of the assignment `failed=1` (assuming this or a similarly descriptive variable name) should be obvious (since it's not in the context of a conditional). (b) By the time the variable value is tested in a conditional, numbers such as `0` and `1` - which definitely could cause confusion - are *out of the picture*, and expressions such as `if ((failed))` and `if ((!failed))` read quite natural to me, even in bash. – mklement0 Jan 24 '14 at 03:06
0

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.)

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • This addresses less than half of the question, and the result is, um, unwieldy. I know elegant is subjective, but this is a step sideways at best and does not address the issue of testing at all. – Caleb Jan 23 '14 at 22:19
  • @Caleb Updated to address the second `if` test, and your comment about not caring about the exact exit code. – John Kugelman Jan 23 '14 at 22:51
-1

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
Dale_Reagan
  • 1,953
  • 14
  • 11