The set
Command
The next line of code in our shim file is:
set -e
If you're using Bash as your shell, you should be able to run help set
without any problems. If you're using the Zsh shell (i.e. the standard shell on a Mac), the help
command is not enabled by default. I recommend you simply open a new Bash shell from within Zsh (by typing bash
at your prompt) if you need to use the help
command.
There are other ways to solve this problem, including solutions which save you from opening Bash first every time. But implementing these would be a digression from our main goal of understanding set
, and I want to keep our forward momentum going. So I've moved my discussion of these options to a blog post here.
Parsing the output of help set
Assuming you were successfully able to run help set
from a Bash terminal window, it should output something like this:
set [ {+|-}options | {+|-}o [ option_name ] ] ... [ {+|-}A [ name ] ]
[ arg ... ]
Set the options for the shell and/or set the positional
parameters, or declare and set an array. If the -s option
is given, it causes the specified arguments to be sorted
before assigning them to the positional parameters (or to
the array name if -A is used). With +s sort arguments
in descending order. For the meaning of the other flags,
see zshoptions(1). Flags may be specified by name using
the -o option. If no option name is supplied with -o, the
current option states are printed: see the description of
setopt below for more information on the format. With +o
they are printed in a form that can be used as input to the
shell.
From the first paragraph, we see the following:
Set the options for the shell and/or set the positional
parameters, or declare and set an array.
So we're setting "options". But that's pretty vague. What options are we talking about?
Shell Options
If we Google "shell options", one of the first results should be from The Linux Documentation Project:
Chapter 33. Options
Options are settings that change shell and/or script behavior.
The set command enables options within a script. At the point in the script where you want the options to take effect, use
set -o option-name
or, in short form,set -option-abbrev
. These two forms are equivalent.#!/bin/bash set -o verbose # Echoes all commands before executing.
#!/bin/bash set -v # Exact same effect as above.
To disable an option within a script, use
set +o option-name
orset +option-abbrev
.
Further down the link, I see a list of options available to set:
Abbreviation Name Effect ... ... ... -e errexit Abort script at first error, when a command exits with non-zero status... ... ... ...
So a "shell option" is simply a setting which controls some aspect of how the shell operates. There are many such options, controlling many such behaviors, and the errexit
option is one of them.
By running set -e
, we tell the shell to turn the errexit
option on. From that point until we turn errexit
off (by running set +e
), the shell will exit as soon as "a command exits with a non-zero status". What does that mean?
Exit Statuses
Shell scripts need a way to communicate whether they've completed successfully or not to their caller. The way this happens is via exit codes. We return an exit code via typing exit
followed by a number. If the script completed successfully, that number is zero. Otherwise, we return a non-zero number which indicates the type of error that occurred during execution. This link from The Linux Documentation Project says:
A successful command returns a 0, while an unsuccessful one returns a non-zero value that usually can be interpreted as an error code. Well-behaved UNIX commands, programs, and utilities return a 0 exit code upon successful completion, though there are some exceptions.
Why does the link say "well-behaved" in this context? That's because a script author is encouraged to observe convention by including an exit code in their script. But nothing forces them to do so- they are free to disregard this convention, possibly to the detriment of the script's users.
We can return different non-zero exit codes to indicate different errors. For example, according to the GNU docs on exit codes:
- "If a command is not found, the child process created to execute it returns a status of
127
. If a command is found but is not executable, the return status is126
." - "All builtins return an exit status of
2
to indicate incorrect usage, generally invalid options or missing arguments." - "When a command terminates on a fatal signal whose number is N, Bash uses the value
128+N
as the exit status."
Exiting immediately vs. continuing execution
Back to set -e
. What the docs are saying is that, if you add set -e
to your bash script and an error occurs, the program exits immediately, as opposed to continuing on with the execution.
OK, but... why do we need set -e
for that? When I write a script in another language, the interpreter exits as soon as an error occurs. Is the helpfile implying that a Bash program would just continue executing if you leave out set -e
and an error occurs?
Let's try an experiment to figure it out.
Experiment- "exit-early" mode
I make 2 Bash scripts, one called foo
and one called bar
. foo
looks like so:
#!/usr/bin/env bash
set -e
./bar
echo "foo ran successfully"
It does the following:
- declares the script as a Bash script
- calls
set -e
in the theory that this will cause any error to prevent the script from continuing - runs the
./bar
script, and - prints a summary line, to prove we've reached the end of the script
In theory, if an error occurs when running ./bar
, our execution should stop and we shouldn't see "foo ran successfully" as output.
Meanwhile, bar
looks like so:
#!/usr/bin/env bash
echo "Inside bar; about to crash..."
exit 1
It does the following:
- declares the script as a Bash script (just like in
foo
) - prints a logline, to prove we're now inside
bar
, and - triggers a non-zero exit code (i.e. an error)
I run chmod +x
on both of these scripts, as we've done before, to make sure they're executable. Then I run ./foo
in my terminal:
$ ./foo
Inside bar; about to crash...
$
We did not see the summary line from foo
printed to the screen. This indicates that the execution inside foo
halted once the bar
script ran into the non-zero exit code.
I also run $?
immediately after running ./foo
. The $?
syntax returns the exit code of the most recent command run in the terminal:
$ echo "$?"
1
$
We get 1
, which is what we'd expect.
Now let's comment out set -e
from foo
:
#!/usr/bin/env bash
# set -e
./bar
echo "foo ran successfully"
Now when we re-run ./foo
, we see the following:
$ ./foo
Inside bar; about to crash...
foo ran successfully
$
This time, we do see the summary logline from foo
. This tells us that the script's execution continues, even though we're still getting the same non-zero exit code from the bar
script.
And when we re-run echo "$?"
, we now see 0
:
$ echo "$?"
0
$
Based on this experiment, we can conclude that set -e
does, in fact, prevent execution from continuing when the script encounters an error.
Why isn't set -e
the default?
But our earlier question remains- why must a developer explicitly include set -e
in their bash script? Why is this not the default?
This question is a little opinion-based, and also involves some historical context. For both of these reasons, it's unlikely the answer will be found in the man
or help
pages. Let's check StackOverflow instead.
One answer says:
Yes, you should always use it...
set -e
should have been the default. The potential for disaster is just too high.
But another answer says:
If your script code checks for errors carefully and properly where necessary, and handles them in an appropriate manner, then you probably don't ever need or want to use
set -e
....
Note that although
set -e
is supposed to cause the shell to exit IFF any untested command fails, it is wise to turn it off again when your code is doing its own error handling as there can easily be weird cases where a command will return a non-zero exit status that you're not expecting, and possibly even such cases that you might not catch in testing, and where sudden fatal termination of your script would leave something in a bad state.
Here, "IFF" means "if and only if".
In addition, we find this post:
Be careful of using
set -e
in init.d scripts. Writing correctinit.d
scripts requires accepting various error exit statuses when daemons are already running or already stopped without aborting theinit.d
script, and commoninit.d
function libraries are not safe to call withset -e
in effect. Forinit.d
scripts, it's often easier to not useset -e
and instead check the result of each command separately.
From reading the answers, I gather that the reason set -e
is not the default is probably because the UNIX authors wanted to give developers more fine-grained control over whether and how to handle different kinds of exceptions. set -e
halts your program immediately whenever any kind of error is triggered, so you don't have to explicitly catch each kind of error separately. Depending on the program you're writing, this might be considered a feature or a bug; it appears to be a matter of preference.
Wrapping Up
One last cool thing about set -e
is that it's not an all-or-nothing operation. If there's one particular section of your script where you would want to exit immediately if an error happens, but the rest of the script doesn't fit that description, then you can call set -e
just before that one section of code, and call set +e
right after it. Again, from this post:
It should be noted that
set -e
can be turned on and off for various sections of a script. It doesn't have to be on for the whole script's execution. It could even be conditionally enabled.
That concludes our look at set -e
. Let's move on to the next line of code.