Bash "strict" mode
Essential options in bash scripts
4 min read
I always start my bash scripts with the following lines:
#!/usr/bin/env bash
set -euo pipefail
# ... rest of the script
And I got asked recently by my coworkers, why. And to be honest with you, I couldn’t remember it from the top of my head, because there’s so much hidden in these two simple lines. It just felt like I was doing it forever, and, quite frankly, automatically at that point.
But what do these lines do exactly? Let’s break it down.
The shebang
#!/usr/bin/env bash is a “shebang” line at the beginning of a script. When operating
system encounters a line starting with #! at the very beginning of a file, it
knows to execute the rest of the file as a command to run the script.
Interestingly, we can put anything in the shebang, for example we can craft this file:
#!/bin/cat
test!
test!!
test!!!
And when you execute it, it just prints itself!
You’ve probably noticed that, instead of specifying /bin/bash directly, it uses
/usr/bin/env bash, and this has a couple of benefits:
- It ensures a script can run on different systems, even if
bashis installed in a non-standard location, such as in a user’s home directory. - It allows a user to use a custom or different version of
bashthey may have installed in their$PATH, rather than the system’s default.
Basically, the env command searches the directories listed in the $PATH environment
variable for the bash executable. And, it executes the script using the first
bash interpreter it finds.
These two little things allow us to do powerful things, like executing the rest
of the script with perl or ruby, for example:
#!/usr/bin/env perl
#!/usr/bin/env ruby
Shell options
The second line (set -euo pipefail) sets three options for the shell:
set -e(can also be written asset -o errexit) tells bash to exit immediately if any command fails with a non-zero exit status. By default,bashdoes not do that! And it makes sense, because usuallybashruns in interactive mode (i.e. when user inputs commands) and it would be pretty annoying if it quit after every failed command 😅. But it’s important to set it for scripts, because almost every time, commands are meant to execute one after the other, and when one fails, script shouldn’t be executed further. Note: it doesn’t trigger in every case, for example, insideifconditions or some compound commands. It catches most errors, but not all.set -u(can also be written asset -o nounset) tells bash to exit immediately if it encounters an undefined variable. Pretty simple! But it’s important when writing scripts, because by default it will just silently error, and just substitute empty string if used in string, for example. Yikes 😬-o pipefailtells bash to make the pipeline’s exit status reflect the failure status of any command in the pipeline, not just the last one. By default, only the exit status of the last command in the pipeline is used as the return status of the entire pipeline.
Additionally, there’s set -x or set -o xtrace option available, that is really
useful for debugging, because it prints every command with expanded arguments before
they are getting executed. Usually, I’m setting this option when writing the script
or when I’m trying to figure something out, but leave it out in “production” scripts.
Read more in this article or in the documentation.
Omitting these options can lead to some disastrous bugs. Here are some YouTube videos that break it down:
There are extended versions of this “strict” mode for bash scripts out there,
that additionally set IFS variable, for example. You can read more in-depth
information in this blog post.
Automatic script creation
I even have this little script to make other scripts!
#!/usr/bin/env bash
set -euo pipefail
if [ ! $# -eq 1 ]; then
echo "mksh takes one argument" 1>&2
exit 1
elif [ -e "$1" ]; then
echo "$1 already exists" 1>&2
exit 1
fi
echo '#!/usr/bin/env bash
set -euo pipefail
'>"$1"
chmod +x "$1"
"${EDITOR:-vim}" "$1"
Shout out to this awesome blog post that opened my eyes to how I can automate this process!
Now I can just run:
mksh some-script.sh
(Given that the path is configured like so in ~/.zshrc):
export PATH=$HOME/bin:$PATH
And voila, the script will be created and I’m ready to edit it instantly.
Wrapping up
It still amazes me how much power hides in those two little lines at the top of a script. They make Bash behave more responsibly, catch mistakes before they cascade, and save hours of debugging you never had to do in the first place.
If you’re writing Bash scripts and not using them yet, give it a try. Your future self will thank you.
That’s it for today, safe scripting y’all 👋