Why does behavior of set -e in a function change when that function is called in a compound command w/ || or &&? - linux

I narrowed my problem to a simple example which puzzles me.
I have tested it with GNU bash 4.2.46 on Centos and 4.3.46 on Ubuntu.
Here is a bash function that returns non-zero (error) return code when called alone but reverses its behavior when I use either && or || to chain another command. It looks like a bug to me. Can someone explain why it is behaving as such?
$ echo $0
/bin/bash
$ function TEST() {( set -e; set -o pipefail; echo OK; false; echo NOT REACHED; )}
$ type TEST
TEST is a function
TEST ()
{
( set -e;
set -o pipefail;
echo OK;
false;
echo NOT REACHED )
}
$ TEST
OK
$ echo $?
1
$ TEST || echo "NON ZERO"
OK
NOT REACHED
$ echo $?
0
$ TEST && echo "UNEXPECTED"
OK
NOT REACHED
UNEXPECTED
$ echo $?
0

What you are seeing is the shell doing what it is specified to do. Non-zero return codes in if statements and loops, and || && logical operators do not trigger detection by set -e or traps. This makes serious error handling more difficult than in other languages.
The root of all problems is that, in the shell, there is no difference between returning a non-zero code as a meaningful and intended status, or as the result of a command failing in an uncontrolled manner. Furthermore the special cases the shell has will disable checking at all depths in the call stack, not just the first one, entirely hiding nested failures from set -e and traps (this is pure evil if you ask me).
Here is a short example that shows what I mean.
#!/bin/bash
nested_function()
{
returnn 0 ; # Voluntarily misspelled
}
test_function()
{
if
[[ some_test ]]
then
nested_function
else
return 1
fi
}
set -e
trap 'echo Will never be called' ERR
if
test_function
then
echo "Test OK"
else
echo "Test failed"
fi
There is an obvious bug in the first function. This function contains nothing that disables error checking, but since it is nested inside an if block (and not even directly, mind you), that error is completely ignored.
You do not have that problem in, say, Java, where a return value is one thing, and an exception is another thing, and where evaluating a return value in an if statement will not prevent an exception at any level in the call stack from doing its job. You have try/catch to handle exceptions, and there is no way to mix exceptions with return codes, they are fundamentally different things (exceptions can be used as return values, but do not trigger the exception mechanism then as when thrown).
If you want to have the same thing in shell programming, you have to build it for yourself. It can be done using a "try" function that is used in front of all calls and keeps state for each nested call, a "throw" equivalent that allows exceptions to be thrown (not as non-zero return codes, but stored inside variables), and trap ... ERR to intercept non-zero return codes and be able to do things like generate a stack trace and trigger a controlled exit (e.g. deleting temporary files, releasing other resources, performing notifications).
With this approach, "exceptions" are explicitly handled failures, and non-zero return codes are bugs. You trade a bit of performance I guess, it is not trivial to implement, and it requires a lot of discipline. In terms of ease of debugging and the level of complexity you can build in your script without being overwhelmed when trying to trace the source of a problem, it is a game changer though.

Handling error codes is the intended behavior of || and &&.
set -e is a great practice in Bash scripting to alert you to any unwanted errors. When using it sometime to chain commands like
set -e
possibly_failing_command || true
echo "This is always reached"
in order to avoid the program stopping.

Related

Dual use bash script - source but also exec subshell? Dynamic return/exit?

My current setup starts with a function that is ostensibly in .bashrc (.bash_it/custom/funcs.bash to be precise)
#!/usr/bin/env bash
function proset() {
. proset-core "$#";
}
proset-core does some decrypting of secrets and exports those secrets to the session, hence the need for the . instead of just running it as a script/subshell.
If something goes wrong in proset-core, I use return instead of exit since I don't want the SSH connection to be dropped.
if [ "${APP_JSON}" = "null" ] ; then
echo -e "\n${redtext}App named $NAME not found in ${APPCONF}. Aborting.${resettext}\n";
return;
fi
This makes sense in the context of the exported proset function, but precludes usage as a script since return isn't valid except from within a function.
Is there a way to detect how it's being called and return one or the other as appropriate?
Just try to return, and exit if it fails.
_retval=$?
return 2>/dev/null || exit "$_retval"
The only case where your code will still be continuing after the return was invoked at top-level (outside of a function) is if you were executed rather than sourced, and should that happen, exiting is the Right Thing.
Make the builtin variable $SHLVL part of $# args as the last arg. Then at test point:
if [ "${#: -1}" -lt $SHLVL ]; then
# SHLVL arg is less than current SHLVL
# we are in a subshell
exit
else
return
fi
Ended up using
calledBy="$(ps -o comm= $PPID)";
if [ "x${calledBy}" = "xsshd" ]; then
return 1;
else
exit 1;
fi
since it didn't require passing anything extra. Anything that might cause this to be problematic please comment. Not too worried about being bash-specific or portable.
Credit: get the name of the caller script in bash script

Generate specific, non-zero return code?

I am working on some piece of python code that calls various linux tools (like ssh) for automation purposes. Right now I am looking into "return code" handling.
Thus: I am looking for a simple way to run some command that gives me a specific non-zero return code; something like
echo "this is a testcommand, that should return with rc=5"
for example. But of course, the above comes back with rc=0.
I know that I can call false, but this will always return with rc=1. I am looking for something that gives me an rc that I can control.
Edit: first answers suggest to exit; but the problem with that: exit is a bash function. So, when I try to run that from within a python script, I get "No such file or directory: exit".
So, I am actually looking for some "binary" tool that gives me that (obviously one can write some simple script to get that; I am just looking if there is something similar to false that is already shipped with any Linux/Unix).
Run exit in a subshell.
$ (exit 5) ; echo $?
5
I have this function defined in .bashrc:
return_errorcode ()
{
return $1
}
So, I can directly use something like
$ return_errorcode 5
$ echo $?
5
Compared to (exit 5); echo $? option, this mechanism saves you a subshell.
This is not exactly what you are asking but custom rc can be achieved through exit command.
echo "this is a test command, that should return with " ;exit 5
echo $?
5

Raise error in a Bash script

I want to raise an error in a Bash script with message "Test cases Failed !!!". How to do this in Bash?
For example:
if [ condition ]; then
raise error "Test cases failed !!!"
fi
This depends on where you want the error message be stored.
You can do the following:
echo "Error!" > logfile.log
exit 125
Or the following:
echo "Error!" 1>&2
exit 64
When you raise an exception you stop the program's execution.
You can also use something like exit xxx where xxx is the error code you may want to return to the operating system (from 0 to 255). Here 125 and 64 are just random codes you can exit with. When you need to indicate to the OS that the program stopped abnormally (eg. an error occurred), you need to pass a non-zero exit code to exit.
As #chepner pointed out, you can do exit 1, which will mean an unspecified error.
Basic error handling
If your test case runner returns a non-zero code for failed tests, you can simply write:
test_handler test_case_x; test_result=$?
if ((test_result != 0)); then
printf '%s\n' "Test case x failed" >&2 # write error message to stderr
exit 1 # or exit $test_result
fi
Or even shorter:
if ! test_handler test_case_x; then
printf '%s\n' "Test case x failed" >&2
exit 1
fi
Or the shortest:
test_handler test_case_x || { printf '%s\n' "Test case x failed" >&2; exit 1; }
To exit with test_handler's exit code:
test_handler test_case_x || { ec=$?; printf '%s\n' "Test case x failed" >&2; exit $ec; }
Advanced error handling
If you want to take a more comprehensive approach, you can have an error handler:
exit_if_error() {
local exit_code=$1
shift
[[ $exit_code ]] && # do nothing if no error code passed
((exit_code != 0)) && { # do nothing if error code is 0
printf 'ERROR: %s\n' "$#" >&2 # we can use better logging here
exit "$exit_code" # we could also check to make sure
# error code is numeric when passed
}
}
then invoke it after running your test case:
run_test_case test_case_x
exit_if_error $? "Test case x failed"
or
run_test_case test_case_x || exit_if_error $? "Test case x failed"
The advantages of having an error handler like exit_if_error are:
we can standardize all the error handling logic such as logging, printing a stack trace, notification, doing cleanup etc., in one place
by making the error handler get the error code as an argument, we can spare the caller from the clutter of if blocks that test exit codes for errors
if we have a signal handler (using trap), we can invoke the error handler from there
Error handling and logging library
Here is a complete implementation of error handling and logging:
https://github.com/codeforester/base/blob/master/lib/stdlib.sh
Related posts
Error handling in Bash
The 'caller' builtin command on Bash Hackers Wiki
Are there any standard exit status codes in Linux?
BashFAQ/105 - Why doesn't set -e (or set -o errexit, or trap ERR) do what I expected?
Equivalent of __FILE__, __LINE__ in Bash
Is there a TRY CATCH command in Bash
To add a stack trace to the error handler, you may want to look at this post: Trace of executed programs called by a Bash script
Ignoring specific errors in a shell script
Catching error codes in a shell pipe
How do I manage log verbosity inside a shell script?
How to log function name and line number in Bash?
Is double square brackets [[ ]] preferable over single square brackets [ ] in Bash?
There are a couple more ways with which you can approach this problem. Assuming one of your requirement is to run a shell script/function containing a few shell commands and check if the script ran successfully and throw errors in case of failures.
The shell commands in generally rely on exit-codes returned to let the shell know if it was successful or failed due to some unexpected events.
So what you want to do falls upon these two categories
exit on error
exit and clean-up on error
Depending on which one you want to do, there are shell options available to use. For the first case, the shell provides an option with set -e and for the second you could do a trap on EXIT
Should I use exit in my script/function?
Using exit generally enhances readability In certain routines, once you know the answer, you want to exit to the calling routine immediately. If the routine is defined in such a way that it doesn’t require any further cleanup once it detects an error, not exiting immediately means that you have to write more code.
So in cases if you need to do clean-up actions on script to make the termination of the script clean, it is preferred to not to use exit.
Should I use set -e for error on exit?
No!
set -e was an attempt to add "automatic error detection" to the shell. Its goal was to cause the shell to abort any time an error occurred, but it comes with a lot of potential pitfalls for example,
The commands that are part of an if test are immune. In the example, if you expect it to break on the test check on the non-existing directory, it wouldn't, it goes through to the else condition
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
Commands in a pipeline other than the last one, are immune. In the example below, because the most recently executed (rightmost) command's exit code is considered ( cat) and it was successful. This could be avoided by setting by the set -o pipefail option but its still a caveat.
set -e
somecommand that fails | cat -
echo survived
Recommended for use - trap on exit
The verdict is if you want to be able to handle an error instead of blindly exiting, instead of using set -e, use a trap on the ERR pseudo signal.
The ERR trap is not to run code when the shell itself exits with a non-zero error code, but when any command run by that shell that is not part of a condition (like in if cmd, or cmd ||) exits with a non-zero exit status.
The general practice is we define an trap handler to provide additional debug information on which line and what cause the exit. Remember the exit code of the last command that caused the ERR signal would still be available at this point.
cleanup() {
exitcode=$?
printf 'error condition hit\n' 1>&2
printf 'exit code returned: %s\n' "$exitcode"
printf 'the command executing at the time of the error was: %s\n' "$BASH_COMMAND"
printf 'command present on line: %d' "${BASH_LINENO[0]}"
# Some more clean up code can be added here before exiting
exit $exitcode
}
and we just use this handler as below on top of the script that is failing
trap cleanup ERR
Putting this together on a simple script that contained false on line 15, the information you would be getting as
error condition hit
exit code returned: 1
the command executing at the time of the error was: false
command present on line: 15
The trap also provides options irrespective of the error to just run the cleanup on shell completion (e.g. your shell script exits), on signal EXIT. You could also trap on multiple signals at the same time. The list of supported signals to trap on can be found on the trap.1p - Linux manual page
Another thing to notice would be to understand that none of the provided methods work if you are dealing with sub-shells are involved in which case, you might need to add your own error handling.
On a sub-shell with set -e wouldn't work. The false is restricted to the sub-shell and never gets propagated to the parent shell. To do the error handling here, add your own logic to do (false) || false
set -e
(false)
echo survived
The same happens with trap also. The logic below wouldn't work for the reasons mentioned above.
trap 'echo error' ERR
(false)
Here's a simple trap that prints the last argument of whatever failed to STDERR, reports the line it failed on, and exits the script with the line number as the exit code. Note these are not always great ideas, but this demonstrates some creative application you could build on.
trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
I put that in a script with a loop to test it. I just check for a hit on some random numbers; you might use actual tests. If I need to bail, I call false (which triggers the trap) with the message I want to throw.
For elaborated functionality, have the trap call a processing function. You can always use a case statement on your arg ($_) if you need to do more cleanup, etc. Assign to a var for a little syntactic sugar -
trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
throw=false
raise=false
while :
do x=$(( $RANDOM % 10 ))
case "$x" in
0) $throw "DIVISION BY ZERO" ;;
3) $raise "MAGIC NUMBER" ;;
*) echo got $x ;;
esac
done
Sample output:
# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6
Obviously, you could
runTest1 "Test1 fails" # message not used if it succeeds
Lots of room for design improvement.
The draw backs include the fact that false isn't pretty (thus the sugar), and other things tripping the trap might look a little stupid. Still, I like this method.
You have 2 options: Redirect the output of the script to a file, Introduce a log file in the script and
Redirecting output to a file:
Here you assume that the script outputs all necessary info, including warning and error messages. You can then redirect the output to a file of your choice.
./runTests &> output.log
The above command redirects both the standard output and the error output to your log file.
Using this approach you don't have to introduce a log file in the script, and so the logic is a tiny bit easier.
Introduce a log file to the script:
In your script add a log file either by hard coding it:
logFile='./path/to/log/file.log'
or passing it by a parameter:
logFile="${1}" # This assumes the first parameter to the script is the log file
It's a good idea to add the timestamp at the time of execution to the log file at the top of the script:
date '+%Y%-m%d-%H%M%S' >> "${logFile}"
You can then redirect your error messages to the log file
if [ condition ]; then
echo "Test cases failed!!" >> "${logFile}";
fi
This will append the error to the log file and continue execution. If you want to stop execution when critical errors occur, you can exit the script:
if [ condition ]; then
echo "Test cases failed!!" >> "${logFile}";
# Clean up if needed
exit 1;
fi
Note that exit 1 indicates that the program stop execution due to an unspecified error. You can customize this if you like.
Using this approach you can customize your logs and have a different log file for each component of your script.
If you have a relatively small script or want to execute somebody else's script without modifying it to the first approach is more suitable.
If you always want the log file to be at the same location, this is the better option of the 2. Also if you have created a big script with multiple components then you may want to log each part differently and the second approach is your only option.
I often find it useful to write a function to handle error messages so the code is cleaner overall.
# Usage: die [exit_code] [error message]
die() {
local code=$? now=$(date +%T.%N)
if [ "$1" -ge 0 ] 2>/dev/null; then # assume $1 is an error code if numeric
code="$1"
shift
fi
echo "$0: ERROR at ${now%???}${1:+: $*}" >&2
exit $code
}
This takes the error code from the previous command and uses it as the default error code when exiting the whole script. It also notes the time, with microseconds where supported (GNU date's %N is nanoseconds, which we truncate to microseconds later).
If the first option is zero or a positive integer, it becomes the exit code and we remove it from the list of options. We then report the message to standard error, with the name of the script, the word "ERROR", and the time (we use parameter expansion to truncate nanoseconds to microseconds, or for non-GNU times, to truncate e.g. 12:34:56.%N to 12:34:56). A colon and space are added after the word ERROR, but only when there is a provided error message. Finally, we exit the script using the previously determined exit code, triggering any traps as normal.
Some examples (assume the code lives in script.sh):
if [ condition ]; then die 123 "condition not met"; fi
# exit code 123, message "script.sh: ERROR at 14:58:01.234564: condition not met"
$command |grep -q condition || die 1 "'$command' lacked 'condition'"
# exit code 1, "script.sh: ERROR at 14:58:55.825626: 'foo' lacked 'condition'"
$command || die
# exit code comes from command's, message "script.sh: ERROR at 14:59:15.575089"

What is the use of "echo || true"?

Why would anyone want to have a code like echo "something" || true ?
I found such a usage on line 92 of /lib/lsb/init-functions on an Ubuntu 14.04:
echo "$pid" || true
I understand that || stands for OR. But I could not find a way to make echo produce any sort of exit code other than zero. And even if it does, who cares?
I mean, using the prefix || true ensures that the exit code is always 0. So if the left-hand side of the double pipe yields anything different from 0, true will be executed and cause the whole line to yield 0.
Why the paranoia with the exit code of a simple echo? Is there anything checking if every single line on /lib/lsb/init-functions finishes with success?
Why the paranoia with the exit code
I believe it is to avoid script exiting due to possible use of:
set -e
at start of the script which would have caused script to exit otherwise.

In a bash script that starts with set -e, can I set the exit code to a different value than the first failed command's?

I'm currently working on some init scripts that should both use set -e and confirm to the Linux Standard Base core specification. Now those two don't really work together:
Since due to the set -e the first command that fails causes the scripts to exit with the return value of the failed command I cannot set the exit status of the script to something LSB conformant for commands that fail with LSB incompatible return values. I could unset -e before each such command, but that's quite a hassle and in that case I'd actually rather not use set -e at all?
I guess another way to achieve the same result as with unset -e would be to do something like
returns_1() { return 1; }
...
cmd_that_fails_with_non_LSB_return_values || returns_1
but that seems quite bulky as well and I'd again have to check each and every command's possible return values.
Is there a way to set the error code returned by the script when it is terminated due to set -e to a fixed value so it would return 1 (i.e. LSB general/unspecified error) no mather what return value the failed command had? Should I not bother with LSB conformant return codes and/or set -e? (this will probably turn into a discussion about the merrits of set -e anyway judging from the amount of search results you get for that)
Small code snippet to illustrate the problem:
#!/bin/bash
# init script for service foo
set -e
start() {
echo "bar"
cmd_fails_with_return_code_3 # script exits with return code 3, not LSB conformant
echo "baz"
}
...
case "$1" in
start)
start
;;
...
esac
From the man page, it appears you can set a trap on ERR:
A trap on ERR, if set, is executed before the shell exits.
I haven't tried this but this would mean something like this might help you:
trap "exit 1" ERR
Be sure to read the man page for other useful options such as -E to inherit the ERR trap in subshells.
You basically have it nailed, although the returns function is superfluous.
cmd_that_fails_with_non_LSB_return_values || exit 1
More typically, you would actually somehow handle the error, if only just to report what happened.
die () {
echo "$0: $#" >&2
exit 1
}
:
cmd_that_fails_with_non_LSB_return_values ||
die "Long command with underscores failed; aborting"
Philosophically, I suppose the purpose of set -e is mainly to "gently remind" (that is, force) you to handle all possible error conditions.

Resources