Bash scripting best practices - Part #1

The easiest way to automate things respectively formulate repetitive tasks is the use of a bash script - simple but powerful. But is it really that simple?

Sure, it isn't! Beside printing some numbers bash scripts may become destructive by deleting files and directories or modifying database contents.

1
2
3
4
#!/bin/bash
for i in {1..5}; do
    print "$i"
done

So, you would be well advised to be careful when writing your scripts. To ease your life I will provide you with some noteworth best practices for robust shell scripts.

Usage

Do you remember this situation? You needed to provide some updates for a software you delivered months ago. So, you checked out the project from your version control, made you changes and had to put your mighty bash script to work which performs some whatever-magic-is-required things. But what were that many options and arguments for?

Just calling the script should come up with some usage details at least. Even better: Provide a complete help with all options and examples for complex operations.

Pipefail and $PIPESTATUS

The return value of a pipeline is the return value of the last command usually.

1
grep -lir foo /bar/baz|sort|xargs

If sort or grep would fail in the above example you would not notice it until xargs would fail.

By using set -o pipefail at the beginning of your scripts errors withing the pipe are no longer hidden from you. The return value of the pipeline will be only 0 if any commands exits with 0.

If you do not want to use the pipefail option there is also an alternative method, the $PIPESTATUS array variable. It contains the exit status of each element of the pipeline.

Exit on error

Use set -o errexit (a.k.a. set -e) to make your script exit when a command fails.

In combination with the pipefail option the script will also exit when a command within a pipeline fails.

Bash options afterwards

You should avoid settings bash options right in the shebang, because the option would not effective when the script is called with bash ./script.sh.

Accidental accessing

Use set -o nounset (a.k.a. set -u) to exit when your script tries to use undeclared variables.

1
declare -r FOO="barbaz"

This safes you from acessing non-existing variables because of spelling errors.

In combination with errexit every access to a non-existing variable would inevitably exit the script, as it triggers an error.

Upshot

Afterwards your bash script should look like this somehow.

 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
#!/usr/bin/env bash
#
# Very sane bash script which helps you doing something useful.
#
# Author: johndoe@example.com
#

set -o errexit -o pipefail -o errexit

declare -r LOGIN="johndoe"
declare -r PASSWORD="secret"

usage() {
    echo "usage: ${0##*/} <email>"
    exit 1
}

do_sth_useful() {
    local -r EMAIL="$1"
    echo "Doing something: ${EMAIL}"
}

main() {
    [[ -z "$1" ]] && usage || do_sth_useful "$1"
}

main "$@"