The Eeuo pipefail option and best practice to write a shell script

Whenever writing a shell script, I mostly always include this option at the beginning of the script

#!/usr/bin/env bash
set -Eeuo pipefail

Sometimes, I also include.

DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)

Explanation

The env command wrapping

  • #!/usr/bin/env bash: trigger the command /usr/bin/env bash <script-path.sh>.
    The difference between using #!/bin/bash is that we do not need to know the exact place of bash in the system (it may be /bin/bash, /usr/bin/bash, or /usr/local/bin/bash, ...). Wrapping the command in an env command makes the script more portable to many environments.

Tip: if you need to add additional parameters to the invoked command, use the -S (--split-string) option of the env command.

#!/usr/bin/env -S awk -f

The -S/--split-string option is not supported in every version of env. In most cases, this shouldn't be your concern because in most major Linux/Unix distribution, the pre-packed env does support this feature.

From my ArchLinux

# env --version
env (GNU coreutils) 8.32
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Richard Mlynarik, David MacKenzie, and Assaf Gordon.

However, if you want to run the script in a docker container that derives from alpine, where alpine derives from busybox, you are unlucky because the shipped version of env in busybox does not support the -S flag. It even does not support the --version flag.

# docker run -it --rm busybox sh
/ # env --version
env: unrecognized option `--version'
BusyBox v1.32.1 (2021-01-12 00:38:40 UTC) multi-call binary.

Usage: env [-iu] [-] [name=value]... [PROG ARGS]

Print the current environment or run PROG after setting up
the specified environment

	-, -i	Start with an empty environment
	-u	Remove variable from the environmen

The set flags

  • -E or -o errtrace: Allow error traps on function calls, subshell environment, and command substitutions.

Script:

#!/usr/bin/env bash

function a {
	echo "a begins"
	false
	echo "a ends"
}

trap "echo \"ERR trap is triggered\"" ERR
echo "errtrace is on"
set -E
a
echo "errtrace is off"
set +E
a

Result:

errtrace is on
a begins
ERR trap is triggered
a ends
errtrace is off
a begins
a ends
  • -e or -o errexit: Exit immediately. When the command exits with a non-zero status, halt the script and exit with that status. By default, this option is off, bash continues the script even if an error occurs.

Script

#!/usr/bin/env bash

function a {
	echo "a begins"
	false
	echo "a ends"
}

echo "errexit is off (default)"
set +e
a
echo "errexit is on"
set -e
a

Result

errexit is off (default)
a begins
a ends
errexit is on
a begins
  • -u or -o nounset: No unset. Prevent the usage of undefined variables. By default, this option is off, bash allows the usage of undefined variables.

Script

#!/usr/bin/env bash

echo "nounset is off"
set +u
echo "unknown var's value is $unknown_var"

echo "errexit is on"
set -u
echo "unknown var's value is $unknown_var"

Result

nounset is off
unknown var's value is 
errexit is on
test.sh: line 9: unknown_var: unbound variable
  • -o pipefail: pipe failure. If any command in the pipeline chain exits with a non-zero status, the whole command will exit with that status.

Script

#!/usr/bin/env bash

echo "pipefail is on"
set -o pipefail
false | true
echo $?

echo "pipefail is off"
set +o pipefail
false | true
echo $?

Result:

pipefail is on
1
pipefail is off
0

In addition, when you want to debug the script, -x or -o xtrace might be useful. This option tells bash to print all running commands.

To turn off a flag

These options can be turn off with set +<option name>, like follows:

set +e
set +E
set +u
set +o pipefail
set +o errtrace
set +o errexit
set +o nounset
set +x
set +o xtrace

The shell options shopt command

Besides, the following shopt (shell option) options are also good to be considered in several cases.

  • shopt -s nullglob: when a pattern has no match, expand them as null. By default, this option is not set and the pattern is expanded as a literal string.

Script

#!/usr/bin/env bash

cd "$(mktemp -d)"

shopt -s nullglob
echo a * b
ls * *.x

shopt -u nullglob
echo a * b
ls * *.x

cd -

Result

a b
a * b
ls: cannot access '*': No such file or directory
ls: cannot access '*.x': No such file or directory
/home/transang/tmp
  • shopt -s dotglob: include files whose names start with a dot (.) in the matching result.

Script

#!/usr/bin/env bash

cd "$(mktemp -d)"

shopt -s dotglob
touch .a
ls *

shopt -u dotglob
touch .a
ls *

rm .a
cd -

Result

.a
ls: cannot access '*': No such file or directory
/home/transang/tmp

To turn off an option with shopt, use, for example shopt -u nullglob.

The DIR variable assignment

The DIR=$(...) assignment assigns the absolute directory path of the script to the DIR variable. It is very helpful when the script is intended to be executed from another directory and the script refers to some resources next to it. For example, /a/b.sh wants to use the resource /a/sibling.txt.

#!/usr/bin/env bash
set -Eeuo pipefail

DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)

echo "Script's Sibling's abs path is ${DIR}/sibling.txt"
echo "CWD's Sibling's abs path is $(readlink -f ./sibling.txt)"

From /c directory, calling this script:

# cd /c
# /a/b.sh
Script's Sibling's abs path is /a/sibling.txt
CWD's Sibling's abs path is /c/sibling.txt

If your shell is sh, this assignment does not work. Refer to this post, to get the equivalent command with sh.


Bash version used in the above sample scripts.

# bash --version
GNU bash, version 5.1.4(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Last but not least, if you are not familiar with writing bash scripts, I highly recommend using the popular shellcheck tool. Most popular editors have plugins for it.


Source:

The Set Builtin (Bash Reference Manual)
The Set Builtin (Bash Reference Manual)
The Shopt Builtin (Bash Reference Manual)
The Shopt Builtin (Bash Reference Manual)