How to catch an error in a linux bash script?
Use set -e
to set exit-on-error mode: if a simple command returns a nonzero status (indicating failure), the shell exits.
Beware that set -e
doesn't always kick in. Commands in test positions are allowed to fail (e.g. if failing_command
, failing_command || fallback
). Commands in subshell only lead to exiting the subshell, not the parent: set -e; (false); echo foo
displays foo
.
Alternatively, or in addition, in bash (and ksh and zsh, but not plain sh), you can specify a command that's executed in case a command returns a nonzero status, with the ERR
trap, e.g. trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR
. Note that in cases like (false); …
, the ERR trap is executed in the subshell, so it can't cause the parent to exit.
Your script changes directories as it runs, which means it won't work
with a series of relative pathnames. You then commented later that you
only wanted to check for directory existence, not the ability to use
cd
, so answers don't need to use cd
at all. Revised. Using tput
and colours from man terminfo
:
#!/bin/bash -u
# OUTPUT-COLORING
red=$( tput setaf 1 )
green=$( tput setaf 2 )
NC=$( tput setaf 0 ) # or perhaps: tput sgr0
# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
# was: do the cd in a sub-shell so it doesn't change our own PWD
# was: if errmsg=$( cd -- "$1" 2>&1 ) ; then
if [ -d "$1" ] ; then
# was: echo "${green}$1${NC}"
printf "%s\n" "${green}$1${NC}"
else
# was: echo "${red}$1${NC}"
printf "%s\n" "${red}$1${NC}"
# was: optional: printf "%s\n" "${red}$1 -- $errmsg${NC}"
fi
}
(Edited to use the more invulnerable printf
instead of the problematic
echo
that might act on escape sequences in the text.)
To expand on the @Gilles' answer:
Indeed, set -e
doesn't work inside commands if you use ||
operator after them, even if you run them in a subshell; e.g., this wouldn't work:
#!/bin/sh
# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer
set -e
outer() {
echo '--> outer'
(inner) || {
exit_code=$?
echo '--> cleanup'
return $exit_code
}
echo '<-- outer'
}
inner() {
set -e
echo '--> inner'
some_failed_command
echo '<-- inner'
}
outer
But ||
operator is needed to prevent returning from the outer function before cleanup.
There is a little trick that can be used to fix this: run the inner command in background, and then immediately wait for it. The wait
builtin will return the exit code of the inner command, and now you're using ||
after wait
, not the inner function, so set -e
works properly inside the latter:
#!/bin/sh
# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup
set -e
outer() {
echo '--> outer'
inner &
wait $! || {
exit_code=$?
echo '--> cleanup'
return $exit_code
}
echo '<-- outer'
}
inner() {
set -e
echo '--> inner'
some_failed_command
echo '<-- inner'
}
outer
Here is the generic function that builds upon this idea. It should work in all POSIX-compatible shells if you remove local
keywords, i.e. replace all local x=y
with just x=y
:
# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
local cmd="$1"; shift
local exit_code=0
local e_was_set=1; if ! is_shell_attribute_set e; then
set -e
e_was_set=0
fi
"$cmd" "$@" &
wait $! || {
exit_code=$?
}
if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
set +e
fi
if [ -n "$CLEANUP" ]; then
RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
return $?
fi
return $exit_code
}
is_shell_attribute_set() { # attribute, like "x"
case "$-" in
*"$1"*) return 0 ;;
*) return 1 ;;
esac
}
Example of usage:
#!/bin/sh
set -e
# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh
main() {
echo "--> main: $@"
CLEANUP=cleanup run inner "$@"
echo "<-- main"
}
inner() {
echo "--> inner: $@"
sleep 0.5; if [ "$1" = 'fail' ]; then
oh_my_god_look_at_this
fi
echo "<-- inner"
}
cleanup() {
echo "--> cleanup: $@"
echo " RUN_CMD = '$RUN_CMD'"
echo " RUN_EXIT_CODE = $RUN_EXIT_CODE"
sleep 0.3
echo '<-- cleanup'
return $RUN_EXIT_CODE
}
main "$@"
Running the example:
$ ./so_3 fail; echo "exit code: $?"
--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
RUN_CMD = 'inner'
RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127
$ ./so_3 pass; echo "exit code: $?"
--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
RUN_CMD = 'inner'
RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0
The only thing that you need to be aware of when using this method is that all modifications of Shell variables done from the command you pass to run
will not propagate to the calling function, because the command runs in a subshell.