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 or set +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 is 126."
  • "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 correct init.d scripts requires accepting various error exit statuses when daemons are already running or already stopped without aborting the init.d script, and common init.d function libraries are not safe to call with set -e in effect. For init.d scripts, it's often easier to not use set -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.