25

I have a bash script that runs three checks over my source code, and then exit 0 if all the commands succeeded, or exit 1 if any of them failed:

#!/bin/bash

test1 ./src/ --test-1=option
exit_1=$?

test2 ./src/ test-2-options
exit_2=$?

test3 ./src/ -t 3 -o options
exit_3=$?

# Exit with error if any of the above failed
[[ $exit_1 -eq 0 && $exit_2 -eq 0 && $exit_3 -eq 0 ]]
exit $?

This code works, but it feels overly long and verbose. Is there some way this can be made nicer? Specifically I am not happy with:

  • Having to run the command, and then assign the exit code to a variable
  • Having to use [[ ... ]], then collect its exit code on the next line to exit with
  • Having to explicitly compare variables to 0, as in [[ $var -eq 0 ]], instead of treating them as booleans

Ideally, the end result would be something more readable like:

exit_1=( test1 ./src/ --test-1=option )
exit_2=( test2 ./src/ test-2-options )
exit_3=( test3 ./src/ -t 3 -o options )

# Exit with error if any of the above failed
exit ( $exit_1 && $exit_2 && $exit_3 )

Some things I have considered:


Getting the error code in to a variable in one line:

exit_1=$( test1 ./src/ --test-1=option )$?
exit_2=$( test2 ./src/ test-2-options )$?
exit_3=$( test3 ./src/ -t 3 -o options )$?

This works, and makes this bit shorter, but I've never seen anyone else use this before. Is this a sensible/sane thing to do? Are there any issues with this?


Just running the tests, and && them together:

test1 ./src/ --test-1=option && \
test2 ./src/ test-2-options && \
test3 ./src/ -t 3 -o options
status=$?

This does not work, as bash short circuits. If test1 fails, test2 and test3 do not run, and I want them all to run.


Detecing errors and exiting using || exit

[[ $exit_1 -eq 0 && $exit_2 -eq 0 && $exit_3 -eq 0 ]] || exit 1

This saves one line of awkward exit codes and variables, but the important bit of exit 1 is now right at the end of the line where you can miss it. Ideally, something like this would work:

exit [[ $exit_1 -eq 0 && $exit_2 -eq 0 && $exit_3 -eq 0 ]]

Of course, this does not work, as [[ returns its output instead of echoing it.

exit $( [[ $exit_1 -eq 0 && $exit_2 -eq 0 && $exit_3 -eq 0 ]] ; echo $? )

does work, but still seems like a horrid cludge


Not explicitly dealing with exit-codes-as-boolean

[[ $exit_1 && $exit_2 && $exit_3 ]]

This does not do what you would hope it would do. The easiest way of && together three return codes stored in variables is with the full $var -eq 0 && .... Surely there is a nicer way?


I know bash is not a nice programming language - if you can even call it that - but is there any way I can make this less awkward?

Tim Heap
  • 1,671
  • 12
  • 11
  • The `var=$(foo)$?` approach is interesting, but puts your test calls in an unnecessary subshell. – kojiro May 03 '13 at 12:14
  • At the very least, your final `exit $?` isn't necessary if the `[[ ... && ... && ... ]]` is the last statement of the script. – chepner May 03 '13 at 12:22
  • Anyone know how to apply this to docker-compose --exit-code-from flag? It allows docker compose to assume the exit code of the service. I want docker compose to have an exit code of 1 if any service has an exit code of 1 and 0 otherwise. – walkerrandophsmith Sep 30 '18 at 04:47
  • https://stackoverflow.com/questions/29568352/using-docker-compose-with-ci-how-to-deal-with-exit-codes-and-daemonized-linked/33291554#answer-45495112 looks like this answer can work. You just list all the containers after running compose and aggregate their exit codes. You won't abort when you see the first exit code of 1, which is behavior you see when using --exit-code-from flag – walkerrandophsmith Sep 30 '18 at 04:53

5 Answers5

19

You can use bash's arithmetic command to OR the exit codes together, and negate the result, to get an exit code of 1 if any of the codes is non-zero. First, an example:

$ ! (( 0 | 0 | 0 )); echo $?
0
$ ! (( 1 | 0 | 0 )); echo $?
1

Now, your script:

#!/bin/bash

test1 ./src/ --test-1=option; exit_1=$?
test2 ./src/ test-2-options;  exit_2=$?   
test3 ./src/ -t 3 -o options; exit_3=$?

# Exit with error if any of the above failed. No need for a final
# call to exit, if this is the last command in the script
! (( $exit_1 || $exit_2 || $exit_3 ))

Or in general, you can accumulate the exit codes as you run an arbitrary number of tests:

#!/bin/bash

# Unfortunately, ||= is not an assignment operator in bash.
# You could use |=, I suppose; you may not be able to assign
# any meaning to any particular non-zero value, though.
test1 ./src/ --test-1=option; (( exit_status = exit_status || $? ))
test2 ./src/ test-2-options;  (( exit_status = exit_status || $? ))  
test3 ./src/ -t 3 -o options; (( exit_status = exit_status || $? ))
# ...
testn ./src "${final_option_list[@]}"; (( exit_status = exit_status || $? ))

exit $exit_status   # 0 if they all succeeded, 1 if any failed
chepner
  • 497,756
  • 71
  • 530
  • 681
  • `! (( $exit_1 || $exit_2 || $exit_3 ))` is very useful. I was trying to avoid putting the `exit_1=$?` assignments on the end of the line, as then some important statements are hidden off the end of long lines. I went with the `$exit_1=$( ... )$?` solution for that, but otherwise used yours. Thanks! – Tim Heap May 03 '13 at 13:26
  • 1
    Actually, scrap that. I can not use `$exit_1=$( ... )$?`, as that collects both the output of the command *and* the return code. I'll just use your whole answer, I think. – Tim Heap May 03 '13 at 13:36
  • Don't you have to do `! $exit_status; exit $?`? – Matthew D. Scholefield Mar 29 '18 at 22:02
  • No @matthew-d-scholefield, exit takes the desired exit code as an argument. [Source](https://www.tldp.org/LDP/abs/html/exit-status.html). – Felix D. Feb 25 '20 at 09:47
  • `! (( $exit_1 || $exit_2 || $exit_3 ))`: Shouldn't that be without `!`? If any of the vars is > 0, I want a `1` as exit code ... otherwise a `0` – sl3dg3 Sep 10 '21 at 13:56
  • Exit statuses and boolean values use different conventions. An exit status of 0 denotes success; a boolean value of 0 is false. Let's say 1 of the commands had an error; you evaluate `! (( 0 || 1 || 0 ))`. The boolean expression is 1, so there was a failure. The exit status for a truthy value is 0, so we negate it to get back an exit status of 1. – chepner Sep 10 '21 at 15:31
10

I was looking for an answer to this myself and decided on a way similar to @chepner's but doesn't use bash's arithmetic expressions:

#!/bin/bash

failed=0
test1 ./src/ --test-1=option || failed=1
test2 ./src/ test-2-options || failed=1
test3 ./src/ -t 3 -o options || failed=1

# Exit with error if any of the above failed
if [ "$failed" -ne 0 ] ; then
    exit
fi

You just set a flag at the beginning and set the flag to 1 if any of the statements fail. The || is the key here.

Chris Middleton
  • 5,654
  • 5
  • 31
  • 68
4

Some improvements

[ $exit_1$exit_2$exit3 = 000 ]
# no exit needed here, script exits with code from last command
Zombo
  • 1
  • 62
  • 391
  • 407
0

You can assign the exit code of a command using this line:

RES1=$(CMD > /dev/null)$?

example:

RES1=$(test1 ./src/ --test-1=option > /dev/null )$?

So your code will be:

exit_1=$( test1 ./src/ --test-1=option > /dev/null )$?
exit_2=$( test2 ./src/ test-2-options > /dev/null )$?
exit_3=$( test3 ./src/ -t 3 -o options > /dev/null )$?

# Exit with error if any of the above failed
exit ( $exit_1 && $exit_2 && $exit_3 )
SatA
  • 537
  • 1
  • 6
  • 15
  • A neat trick I didn't know, but this creates an unnecessary subshell, so I can't really upvote it in good conscience. – kojiro May 03 '13 at 12:01
  • As the test scripts fire up a test web server and `wget --mirror` to test for broken links, diff multiple folders, against one another, and grep across some files for remaining `TODO`s left in the documents, firing up a subshell is something I can live with! This is still good to know though, if speed and efficiency is ever an issue. – Tim Heap May 03 '13 at 13:24
  • Actually, this does not work at all! If the commands in the subshell produce any output (which my commands do), then the output is assigned to the variable. This means that `out=$( echo 'foo' ; exit 1 )$?` results in `$out` being `'foo1'`. – Tim Heap May 03 '13 at 13:35
  • Sorry, my mistake. its been a while since I used that. Anyway, I fixed the answer, now it works. I also removed the 'if' note since I am not sure on that one. – SatA May 03 '13 at 13:45
  • Unfortunately I need the output of the commands to be printed, so redirecting to /dev/null is not an option. I'll just collect the exit status after the command has run – Tim Heap May 04 '13 at 01:24
0

Why do you need three different variables?

fn() {
    local -i st
    test1 ./src/ --test-1=option
    st=$?

    test2 ./src/ test-2-options
    (( st = ($? || st) )) # Use arithmetic expression to or the values at each step

    test3 ./src/ -t 3 -o options
    (( 0 == ($? || st) ))
}
kojiro
  • 74,557
  • 19
  • 143
  • 201