How to safely exit early from a bash script? - bash

I know there are several SO questions on exit vs. return in bash scripts (e.g. here).
On this topic, but different from existing questions, I believe, I'd like to know if there is a "best practice" for how to safely implement "early return" from a bash script such that the user's current shell is not exited if they source the script.
Answers such as this seem based on "exit", but if the script is sourced, i.e. run with a "." (dot space) prefix, the script runs in the current shell's context, in which case exit statements have the effect of exiting the current shell. I assume this is an undesirable result because a script doesn't know if it is being sourced or being run in a subshell - if the former, the user may unexpectedly have his shell disappear. Is there a way/best practice for early-returns to not exit the current shell if the caller sources it?
E.g. This script...
#! /usr/bin/bash
# f.sh
func()
{
return 42
}
func
retVal=$?
if [ "${retVal}" -ne 0 ]; then
exit "${retVal}"
# return ${retVal} # Can't do this; I get a "./f.sh: line 13: return: can only `return' from a function or sourced script"
fi
echo "don't wanna reach here"
...runs without killing my current shell if it is run from a subshell...
> ./f.sh
>
...but kills my current shell if it is sourced:
> . ./f.sh
One idea that comes to mind is to nest code within coditionals so that there is no explicit exit statement, but my C/C++ bias makes think of early-returns as aesthetically preferable to nested code. Are there other solutions that are truly "early return"?

The most common solution to bail out of a script without causing the parent shell to terminate is to try return first. If it fails then exit.
Your code will look like this:
#! /usr/bin/bash
# f.sh
func()
{
return 42
}
func
retVal=$?
if [ "${retVal}" -ne 0 ]; then
return ${retVal} 2>/dev/null # this will attempt to return
exit "${retVal}" # this will get executed if the above failed.
fi
echo "don't wanna reach here"
You can also use return ${retVal} 2>/dev/null || exit "${retVal}".
Hope this helps.

Related

How can I write and reference global bash scripts which halt the parent script's execution upon conditions?

I have written a simple script
get-consent-to-continue.sh
echo Would you like to continue [y/n]?
read response
if [ "${response}" != 'y' ];
then
exit 1
fi
I have added this script to ~/.bashrc as an alias
~/.bashrc
alias getConsentToContinue="source ~/.../get-consent-to-continue.sh"
My goal is to be able to call this from another script
~/.../do-stuff.sh
#!/usr/bin/env bash
# do stuff
getConsentToContinue
# do other stuff IF given consent, ELSE stop execution without closing terminal
Goal
I want to be able to
bash ~/.../do-stuff.sh
And then, when getConsentToContinue is called, if I respond with anything != 'y', then do-stuff.sh stops running without closing the terminal window.
The Problem
When I run
bash ~/.../do-stuff.sh
the alias is not accessible.
When I run
source ~/.../do-stuff.sh
Then the whole terminal closes when I respond with 'n'.
I just want to cleanly reuse this getConsentToContinue script to short-circuit execution of whatever script happens to be calling it. It's just for personal use when automating repetitive tasks.
A script can't force its parent script to exit, unless you source the script (since it's then executing in the same shell process).
Use an if statement to test how getConsentToContinue exited.
if ! getConsentToContinue
then
exit 1
fi
or more compactly
getConsentToContinue || exit
You could pass the PID of the calling script
For instance say you have a parent script called parent.sh:
# do stuff
echo "foo"
check_before_proceed $$
echo "bar"
Then, your check_before_proceed script would look like:
#!/bin/sh
echo Would you like to continue [y/n]?
read response
if [ "${response}" != 'y' ];then
kill -9 $1
fi
The $$ denotes the PID of the parent.sh script itself, you could find the relevant docs here. When we pass $$ as a parameter to the check_before_proceed script, then we would have access to the PID of the running parent.sh via the positional parameter$1 (see positional parameters)
Note: in my example, the check_before_proceed script would need to be accessible on $PATH

Exiting a shell-script at the end with a non-zero code if any command fails

I am making a shell script that runs a bunch of tests as part of a CI pipeline. I would like to run all of the tests (I DO NOT WANT TO EXIT EARLY if one test fails). Then, at the end of the script, I would like to return with a negative exit code if any of the tests failed.
Any help would be appreciated. I feel like this would be a very common use case, but I wasn't able to find a solution with a bit of research. I am pretty sure that I don't want set -e, since this exits early.
My current idea is to create a flag to keep track of any failed tests:
flag=0
pytest -s || flag=1
go test -v ./... || flag=1
exit $flag
This seems strange, and like more work than necessary, but I am new to bash scripts. Am I missing something?
One possible way would be to catch the non-zero exit code via trap with ERR. Assuming your tests don't contain pipelines | and just return the error code straight to the shell launched, you could do
#!/usr/bin/env bash
exitCodeArray=()
onFailure() {
exitCodeArray+=( "$?" )
}
trap onFailure ERR
# Add all your tests here
addNumbers () {
local IFS='+'
printf "%s" "$(( $* ))"
}
Add your tests anywhere after the above snippet. So we keep adding the exit code to the array whenever a test returns a non-zero return code. So for the final assertion we check if the sum of the array elements is 0, because in an ideal case all cases should return that if it is successful. We reset the trap set before
trap '' ERR
if (( $(addNumbers "${exitCodeArray[#]}") )); then
printf 'some of your tests failed\n' >&2
exit -1
fi
The only way I could imagine using less code is if the shell had some sort of special all compound command that might look something like
# hypothetical all command
all do
pytest -s
go test -v ./...
done
whose exit status is the logical or of the exit statuses of the contained command. (An analogous any command would have the logical and of its commands' exit statuses as its own exit status.)
Lacking such a command, you current approach is what I would use. You could adapt #melpomene's suggestion of a chk function (which I would call after a command rather than having it call your command so that it works with arbitrary shell commands):
chk () { flag=$(( flag | $? )); }
flag=0
pytest -s; chk
go test -v ./...; chk
exit "$flag"
If you aren't using it for anything else, you could abuse the DEBUG trap to update flag before each command.
trap 'flag=$((flag | $?))' DEBUG
pytest -s
go test -v ./...
exit "$flag"
(Be aware that a debug trap executes before the shell executes another command, not immediately after a command is executed. It's possible that the only time this matters is if you expect the trap to fire between the last command completing and the shell exiting, but it's still worth being aware of.)
I vote for Inian's answer. Traps seem like the perfect way to go.
That said, you might also streamline things by use of arrays.
#!/usr/bin/env bash
testlist=(
"pytest -s"
"go test -v ./..."
)
for this in "${testlist[#]}"; do
$this || flag=1
done
exit $flag
You could of course fetch the content of the array from another file, if you wanted to make a more generic test harness that could be used by multiple tools. Heck, mapfile could be a good way to populate an array.

Can someone explain how unix exit commands work?

I have read about unix exit commands but please can someone tell me how they work exactly.
I mean what is their purpose and how can they be used.
Also i see people talking about success = 0 or something and i dont have a clue what they mean by this.
the "exit" command exits the shell script
echo "A"
exit 1
echo "B"
In the above example 'echo "B"' is not executed because of the exit statement.
It's like a return statement in normal progemming languages. The expression after the exit is the return value. Convention is that 0 means "Success" other values means an error.
So if the above script is called q.sh, than this script can be called from an other script:
sh ./q.sh
echo $?
The code "$?" means "exit" value of the last shell script.
Above script prints "1"

How to return the error from a sourced bash script

I'm fairly new to bash scripting. I have 4 nested bash scripts, and i'm having trouble propogating the error from the 4th script appropriately. eg:
script1.sh:
source script2.sh
<check for error and display appropriate message>
script2.sh:
source script3.sh param_1
<return to script1 on error>
source script3.sh param_2
<return to script1 on error>
source script3.sh param_n
<return to script1 on error>
script3.sh
<some processing>
script4.sh
echo "this statement is not reached"
return $?
script4.sh
<some processing>
exit $?
My requirements are:
I need to define an associative array in script1, which is populated in script2 and available in scope of script3. I think the only way to do this is source script2 and script3
script4 is executed not sourced, as this script can also be executed independantly of these parent scripts
This thread talked about using the return statement to return from a sourced bash script, but as script4 is executed i need to exit. I don't understand why the exit statement in script4 causes both the original shell and the sub shell to terminate? Surely it should only exit the sub shell?
Do i need to look at signals and traps?
Thanks for any help
You can keep set -e enabled if you want. Then you'll have to be more careful about invoking a script where you know the exit status may be non-zero:
script3.sh
<some processing>
if script4.sh; then
rc=0
else
rc=$?
fi
echo "script4 complete"
return $rc
See https://www.gnu.org/software/bash/manual/bashref.html#index-set
IMO, using set -e is appropriate if you truly want to abort your program for any error. Here, where you want to <check for error and display appropriate message>, that's clearly not the case.
Best practice is to be explicit. If your code is always going to be sourced, and thus return will be valid, simply::
source foo || return
return uses the exit status of the immediately prior command as its default value, so there's no need to capture and then pass it through.
If you don't know if you'll be sourced or executed, this gets a bit more complicated:
source foo || { rc=$?; return "$rc" 2>/dev/null || exit "$rc" }

How to make bash execute a script every time it exits?

I want to execute some commands every time I exit from bash, but cannot find a way to do it.
There is a ~/.bash_logout file for when you are logging out, but normally we use interactive shell instead of login shell, so this is not very useful for this purpose.
Is there a way to do this? Thanks!
You can trap the EXIT signal.
exit_handler () {
# code to run on exit
}
trap 'exit_handler' EXIT
Techinically, trap exit_handler EXIT would work as well. I quoted it to emphasize that the first argument to trap is a string that is essentially passed to eval, and not necessarily a single function name. You could just as easily write
trap 'do_this; do_that; if [[ $PICKY == yes ]]; then one_more_thing; fi' EXIT
rather than gather your code into a single function.

Resources