Wiki

Shell Script

Robust Shell Scripts

First of all, if your script is over 100 lines, it should probably be rewritten in Python or Go.

Bash Options

Always do set -o errexit -o pipefail -o nounset at the start of the script.

  • -o errexit: Abort when a command exits with non-zero status (except in until, while, if)
  • -o pipefail: Causes a pipeline to return the exit status of the last command in the pipe that returned a non-zero return value.
  • -o nounset: Attempt to use undefined variable causes an error and forces an exit

Trapping Deferred Calls

1
2
3
trap '{
  rm -f '"${TEMP_FILE}" "${LOCK_FILE}"'
}' INT TERM EXIT
  • INT: capturing SIGINT (Interrupt). ctrl-c sends such a signal.
  • TERM: capturing SIGTERM (Terminate).
  • EXIT is a pseudo-signal triggered when the script exits, either through reaching the end of the script, an exit command, or a failing command when set -o errexit.

Just Quote Everything

Variable expansion:

  • Good: "$my_var"
  • Bad: $my_var

Command substitution:

  • Good: "$(cmd)"
  • Bad: $(cmd)

Linter and Formatter

  • Use shellcheck, a shell script static analysis tool, to improve your script.
  • Use shfmt to format your script

Further Reading

Bash Pitfalls: a compilation of common mistakes made by bash users.

Snippets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# log interpolates and writes message to stderr with timestamp.
log() {
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}

# Retry a command up to a specific numer of times until it exits successfully,
# with exponential back off.
#
#  $ retry 5 echo Hello
#  Hello
#
#  $ retry 5 false
#  Retry 1/5 "false" exited 1, retrying in 1 seconds...
#  Retry 2/5 "false" exited 1, retrying in 2 seconds...
#  Retry 3/5 "false" exited 1, retrying in 4 seconds...
#  Retry 4/5 "false" exited 1, retrying in 8 seconds...
#  Retry 5/5 "false" exited 1, no more retries left.
#
retry() {
	local retries=$1
	shift

	local count=0
	until "$@"; do
		exit=$?
		wait=$((2 ** count))
		count=$((count + 1))
		if [ $count -lt "$retries" ]; then
			log "Retry $count/$retries \"$*\" exited $exit, retrying in $wait seconds..."
			sleep $wait
		else
			log "Retry $count/$retries \"$*\" exited $exit, no more retries left."
			return $exit
		fi
	done
	return 0
}