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 ofbash
in the system (it may be/bin/bash
,/usr/bin/bash
, or/usr/local/bin/bash
, ...). Wrapping the command in anenv
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: