Best Practices for Shell Scripts

The shell is an odd beast. Although it goes against every current trend in software engineering (strong typing, compile checks over runtime checks, ...), shell scripts are here to stay, and still constitute an important part of every developer's life.

The weird thing about shell scripts is that even strong advocates of good practices gladly forget all they know when it comes to shell scripting.

Versioning? Why bother, it's disposable code.

Code quality? That's just a shell script, it's garbage anyway.

Testing? Nah. There aren't any decent tools for that.

Wrong, wrong, and wrong. Shell scripts have value. Everything you do for real code should be done for non trivial shell scripts even for a one-time script. That includes versioning, code reviews, continuous integration, static code analysis, and testing.

Here is a summary of everything that can, and should be done when writing shell scripts.

Note: This article will use Bash as a reference shell. Most of the content can be transposed to other POSIX compliant shells.

Keep your scripts in version control

Keeping shell scripts under version control has multiple advantages:

Keeping shell scripts under version control has multiple advantages:

Please, from now on, version all your shell scripts before running them. Have someone reviewing your scripts in priority before executing them in production. It's not a waste of your coworkers' time, it's a time saver for the team.

Improve the quality of your scripts with ShellCheck

Although you can check the syntactic validity of your scripts with the command bash -n, much powerful tools exist.

ShellCheck is a static code analysis tool for shell scripts. It's really an awesome tool which will help you improve your skills as you use it. So do use it. You can install it globally on your machine, use it in your continuous integration, and it even works in your favourite editor. There really are no downsides to using ShellCheck and it can save you from yourself.

If Steam had used ShellCheck in 2015, this line would never have made it to production:

rm -rf "$STEAMROOT/"*

This code violates the SC2115 rule from ShellCheck.

Use Bash unofficial strict mode

The unofficial strict mode comes from Aaron Maxwell's article "Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)". He suggests to start every Bash script with the following lines:

set -euo pipefail

For each line

while read -r line; do
    echo "... $line ..."
done <<< "$list"

Default values

: "${S3_HOST:=""}"
: "${S3_BUCKET_NAME:="foo/bar"}"
: "${S3_ACCESS_KEY:-""}"
: "${S3_SECRET_KEY:-""}"

Get the source directory of a Bash script from within the script itself

get_source_directory_of_script_self() {
    local source
    local dir;
    while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink
        dir="$( cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd )"
        source="$(readlink "$source")"
        [[ $source != /* ]] && source="$dir/$source" # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located
    dir="$( cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd )"
    echo "$dir"

Loop over comma separated string (tuple'y list)

for item in $(echo $variable | sed "s/,/ /g")
    echo "$item"

Get variable from tuple by index

get_item_by_idx_from_tuple() {
        local idx
        local tuple
        local list
        IFS=',' read -ra list <<< "$tuple"
        echo "${list[$idx]}"

# returns Walialu
get_item_by_idx_from_tuple 2 "Foo,Bar,Walialu,Baz,2000"

Check if script is already running

# Check if parallel run and if not enabled exit early
if pidof -x "$(basename "$0")" -o $$ >/dev/null; then
        echo "Script is already running! Exiting now!"
        exit 1