Bash: Exit and cleanup on error - bash

In my Bash scripts, I would like to make sure that the script exits as soon as there is an error. (E.g., to avoid a mistaken rm -f * after a failed cd some_directory.) For this reason, I always use the -e flag for bash.
Now, I would also like to execute some cleanup code in some of my scripts. From this blog post I gathered
#!/bin/bash
cd invalid_directory
echo ':('
function clean_up {
echo "> clean_up"
exit 0
}
trap clean_up EXIT
The output I get is
./test.sh: line 3: cd: invalid_directory: No such file or directory
:(
> clean_up
so it does what's advertised. However, when using -e for bash, I'm only getting
./test.sh: line 3: cd: invalid_directory: No such file or directory
so the script exits without calling clean_up.
How can I have a bash script exit at all errors and call a clean up script every time?

You are never reaching the trap command; your shell exits before the trap is configured.
set -e
clean_up () {
ARG=$?
echo "> clean_up"
exit $ARG
}
trap clean_up EXIT
cd invalid_directory
echo "Shouldn't reach this"
However, it's better to do your own error handling. You often want to vary your behavior depending on the exact reason why your script is exiting, something that is more complicated to do if you are running a single handler for all exits (even if you restrict your trap to ERR instead of EXIT).
cd invalid_directory || { echo "cd to invalid_directory failed" >&2; exit 1; }
echo "Shouldn't reach this"
This doesn't mean you have to abandon your clean_up function. It will still be executed for explicit exits, but it should be restricted to code that should run no matter why your script exits. You can also put a trap on ERR to execute code that should only be executed if you script is exiting with a non-zero exit status.

Related

How can i show error message for a particular command , if bash script terminates due to set -e

I want to display a error log line for one particular command when its return value is nonzero .
I am using ' set -e ' for terminating if any command returns nonzero value along with ' trap ' for this
#!/bin/bash
set -e
log_report() {
echo "Error on line $1"
}
trap 'log_report $LINENO' ERR
echo "starting ..."
first_badcommand
echo "running"
second_badcommd
OUTPUT:
starting ...
/tmp/test1.sh: line 10: first_badcommand: command not found
Error on line 10
since i use set -e the script exit and showing my error log for first_badcommand.. itself.
I want to exit with error log for only a particular command giving non zero return code
and for rest of commands giving non zero return code, exit without error log
After clarification, it appears that the requirement is to exit the script if any error happens, but that the commands which are described as "badcommand" in the question, might or might not fail.
In this answer, I am naming the commands simply first_command etc, to reflect the fact they might or might not fail.
The set -e command, as suggested in the question, will indeed terminate the script if an error occurs, and the trap ... ERR installs a handler which will run after an error (and before the script exits where set -e has been used).
In this case, you should:
wait until the trap is required before installing it (it does not need to be done at/near the start of the script)
disable the trap again when it is no longer required, using trap - ERR
so that commands to enable and disable the trap surround the command for which the trap is required.
For example:
#!/bin/bash
set -e
log_report() {
echo "Error on line $1"
}
echo "starting ..."
first_command
trap 'log_report $LINENO' ERR
echo "running"
second_command
trap - ERR
echo "still running"
third_command
This will exit if any command fails (because of the set -e at the top), but the trap will only be run if second_command fails.
(Note also that set -e similarly does not need to be applied at the start of the script. It can be enabled at any point, and disabled again using set +e. But in this example, it appears that the exit-on-error behaviour is required throughout.)
set -e does not stop you from checking the status of a command line you'd otherwise do:
if some_command
then
echo "It succeeded"
else
echo "It failed. Some message here."
exit 1
fi

trap exec return code in shell script

I have to run a command using exec in a shell script, and I need to trap the exit code in case of error and run another command e.g
#!/bin/sh
set +e
exec command_that_will_fail
if [ $? -eq 1 ]; then
echo "command failed, running another command"
fi
I understand that exec replaces the current shell and carries on, my problem is that I need to run another command if the exec is not sucessfull.
Your code works if there's some immediate error when it tries to run the process:
$ echo 1
1
$ echo $?
0
$ exec asd123
-bash: exec: asd123: not found
$ echo $?
127
If the executable file was found, and is started, then it will not return, because it will overtake the whole script and never return to bash again.
For example this never returns:
$ exec grep asd /dev/null
(the exit code of grep is 1, but the parent shell is overtaken, so nobody can check)
If you want to get an exit code from the process in this case, you have to start it as a subprocess, i.e. not using exec (just command_that_will_fail). In this case the bash process will act as a supervisor that waits until the subprocess finishes and can inspect the exit code.

Catching all bad signals for called commands in Bash script

We are creating a bash script for a build server. We want to ensure that when we execute a bash command inside the script, it returns with a signal of 0. If it does not, we want execution to stop. Our solution so far is to do:
#some command
if [ $? -ne 0 ] ; then
#handle error
fi
after every command that could cause this problem. This makes the code quite long and doesn't seem very elegant. We could use a bash function, perhaps. Although working with $? can be a bit tricky, and we would still have to call the function after every command. Is there a better way? I've looked at the trap command but it seems to only do signal handling with the bash script I am writing, not any commands I call.
The robust, canonical way of doing this is:
#!/bin/bash
die() {
local ret=$?
echo >&2 "$*"
exit "$ret"
}
./configure || die "Project can't be configured"
make || die "Project doesn't build"
make install || die "Installation failed"
The fragile, convenient way of doing this is set -e:
#!/bin/bash
set -e # Script will exit in many (but not all) cases if a command fails
./configure
make
make install
or equivalently (with a custom error message):
#!/bin/bash
# Will be called for many (but not all) commands that fail
trap 'ret=$?; echo >&2 "Failure on line $LINENO, exiting."; exit $ret' ERR
./configure
make
make install
For the latter two, the script will not exit for any command that is part of a conditional statement or &&/||, so while:
backup() {
set -e
scp backup.tar.gz user#host:/backup
rm backup.tar.gz
}
backup
will correctly avoid executing rm if the transfer fails, later inserting a feature like this:
if backup
then
mail -s "Backup completed successfully" user#example.com
fi
will make the backup stop exiting on failure and accidentally delete backups.

Propagating exit code to caller in case of a shell error from script having an exit trap

Is it possible to propagate an exit code to the caller in case of a syntax error in a Bash script with an EXIT trap? For example, if I have:
#! /bin/bash
set -eu
trap "echo dying!!" EXIT
echo yeah
echo $UNBOUND_VARIABLE
echo boo
Then, running it gives an exit code 0 even if the script did not really end successfully:
$ bash test.sh
yeah
test.sh: line 8: UNBOUND_VARIABLE: unbound variable
dying!!
$ echo $?
0
But if I comment out the exit trap, the script returns 1. Alternatively, if I replace the line with the unbound variable with a command that returns nonzero (e.g. /bin/false), that exit value is propagated as I would like it to.
The shell exits with the result of the last executed command. In your trap case, that's echo, which usually returns with success.
To propagate your value, simply exit with it.
#!/bin/bash
set -eu
die() {
echo "Dying!!"
exit "$1"
}
trap 'die $?' EXIT
echo yeah
echo $unbound
echo boo
Also note that set -e is considered harmful -- it makes you think the script will exit if a command fails, which it won't always do.
This behavior is related to different Bash versions. The original script works as expected on Bash 4.2 but not on 3.2. Having the error-prone code in a separate script file and running it in a subshell works around problems in earlier Bash versions:
#!/bin/bash
$BASH sub.sh
RETVAL=$?
if [[ "$RETVAL" != "0" ]]; then
echo "Dying!! Exit code: $RETVAL"
fi
sub.sh:
set -eu
echo yeah
echo $UNBOUND_VARIABLE
echo boo

How to stop execution in script which may have been sourced or directly executed

If my script gets sourced with
. ./my_script.sh
source ./my_script.sh
then to stop execution in the script, I would use return.
If my script is directly executed with
./my_script.sh
bash ./my_script.sh
then I'd insert an exit.
If I don't know whether the user will source it or directly execute it, how can I cleanly stop the script without killing the terminal it was called from?
Preferably, the code snippet should be able to terminate the script even if it were placed inside one of the script's functions.
Try the following:
ec=0 # determine the desired exit code
return $ec 2>/dev/null || exit $ec
return will succeed if the script is being sourced, otherwise exit will kick in. The 2>/dev/null suppresses the error message in case the script is not sourced.
The net effect is that the script will terminate with the desired exit / return code, regardless of whether it was sourced or not.
Update: The OP wants to be able to exit the script from inside a function within the script.
The only way I can think of is to place all of your script's code in a subshell and call exit from inside the functions inside that subshell; then place the return 2>/dev/null || exit command after the subshell (as the only statement outside the subshell):
#!/usr/bin/env bash
( # subshell to place all code in
foo() {
exit 1 # exit the subshell
}
foo # invoke the function
)
# Terminate script with exit code from subshell.
ec=$?; return $ec 2>/dev/null || exit $ec
Here's one way to find out which method was used to invoke the script.
if [[ "$0" == "bash" ]]; then
echo "source 'file' was used"
else
echo "bash 'file' was used"
fi
Update, In response to comment by #mklement0:
If your script is named my_script.sh, you can use
b=$(basename "$0")
if [[ "$b" == "my_script.sh" ]]; then
echo "bash 'file' was used"
else
echo "source 'file' was used"
fi

Resources