The following script has a syntax error or error of some sort:

#!/usr/bin/env bash
set -euo pipefail

if [ ! -f /custom.log]; then
  echo "test"
fi
abcxyz

The script fails with output of:

./test.sh: line 4: [: missing `]'
./test.sh: line 7: abcxyz: command not found

I'm not concerned how to fix this script, but how can I prevent the script from proceeding further if encounters this error? I would have thought set -e would enforce this behavior.

  • This error is identified by shellcheck. – Chris Stryczynski Nov 29 '17 at 11:52
  • Note that there's a difference between the syntax of the shell language and the syntaxes of individual commands. – JdeBP Nov 29 '17 at 12:01
  • And in this case it is the syntax of the individual command of [? – Chris Stryczynski Nov 29 '17 at 12:06
  • How could it tell the difference between an error in the conditional and the condition just returning false? Both cases are just a nonzero exit code. And even though [ is a builtin in modern shells, it's compatible with /bin/[, so it can't do special I'm-part-of-the-shell tricks; those are reserved for [[ (and indeed, if it did do special tricks, that would be a further set of incompatibilities with other shells where it didn't). – Charles Duffy Nov 29 '17 at 15:59
  • BTW, set -e is a very controversial feature for other reasons as well. See the exercises (skipping the parable if in a hurry) in BashFAQ #105, and the table showing ways in which its behavior is incompatible between different shells at in-ulm.de/~mascheck/various/set-e – Charles Duffy Nov 29 '17 at 16:01
up vote 9 down vote accepted

set -e doesn't trigger on failing commands that are used as conditions like in the condition section of if/while/until constructs or on the left of a ||, && or in functions, subshells, sourced files, evaled code that are invoked under those conditions.

If it did, then:

if [ ! -f /custom.log ]; then

Would exit the script if /custom.log was a regular file as [ would then also exit with a non-zero exit status.

The [ builtin command of the bash shell (and most other implementations) exits with a 1 status if the tested condition is not met, and 2 if there's a syntax error (not all syntax errors though, for instance, not in [ -v 'a[+]' ]). POSIX requires the exit status to be greater than 1 in case of error.

So you could choose to exit the script if a command exits with a code greater than 1 regardless of whether it's used in a condition or not with something like:

shopt -s extdebug # make sure the DEBUG trap propagates to subshells
trap '(($?>1 && (ret=$?))) && exit "$ret"' DEBUG
[ -f / ] || echo / not a regular file # OK
[ -f /] || echo was a syntax error # causes an exit, not output
echo not reached

Note that you can't use the ERR trap for that as the ERR trap is only run in the same condition as those that trigger the exit by set -e.

Now, beware of the implications. For instance, that would cause a:

if grep -qs pattern /file; then
  echo pattern was found in /file
fi

to exit if /file didn't exist or wasn't readable, as grep returns with a 2 status in that case, even though with -s, the intention was clearly to ignore those cases.

So you'll need to beware of upon which conditions the commands you use in your conditions may exit with a status greater than 1. To work around those, you'd need something like:

if sh -c 'grep -sq pattern / file || exit 1'; then...

You could restrict the exit upon exit status greater than 1 to the [ or test command with something like:

unset -v previous_BASH_COMMAND
trap '
  case $previous_BASH_COMMAND in
    ("[ "* | "test "*) (($?>1 && (ret=$?))) && exit "$ret"
  esac
  previous_BASH_COMMAND=$BASH_COMMAND' DEBUG

That has a few limitations. In

echo x
([ -f/]; echo y)

That would cause the subshell to exit, but not the parent as the $previous_BASH_COMMAND has not been set there. And in:

[ -f / ] && echo a regular file
(grep -qs foo /file && echo foo in /file)
echo here

The shell would be exited upon running echo here, because $? would be 2 and $previous_BASH_COMMAND was [ -f / ].

In any case, things like

[ -f /] | cat
export var="$([ -f /])"

could not be detected as the exit status is not propagated to the parent shell process (except with the pipefail option in the first case).

Now, I'm not sure it's worth the trouble adding this kind of (brittle) detection at run time, when the error is easily detectable at development time (when you write and test your script).

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.