5 tips for solid Bash code

gmarik 3 min
Table Of Contents ↓

Here are few things that help improve my Bash code when a script grows more than just couple lines but not enough to be rewritten with Go.

Tip 1: fail early

Probably is well known but still worth mentioning:

  -e      Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command, exits with a non-zero status.

See man bash's “SHELL BUILTIN COMMANDS” chapter. The inverse is set +e and it disables “fail early” mode.

Tip 2: debug output

The simplest and most widely applied debugging technique is called “printf” and involves injecting printing statement where necessary.

While it’s possible to sprinkle bash scripts with printfs there’s more convenient way:

 -x     After expanding each simple command, for command, case command, select command, or arithmetic for command, display the expanded value of PS4, followed by the command and its expanded arguments or associated word list.

Unfortunately the official explanation is kinda cryptic but the idea is that the code being executed is dumped to stdout prefixed with number of + s which correspond to call stack level.

Using set -x removes the need to add/remove debugging statements which can be a significant time saver.

Example use follows below.

Tip 3: ensure variable is set

 ${parameter:?word}     Display Error if Null or Unset. If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.

Is one of the Bash’s “Parameter Expansion"s and it lets ensuring a variable is set.

An example:

$ echo ${HOME:?"Must be set"}
/Users/gmarik

$ echo ${DOME:?"Must be set"}
-bash: DOME: Must be set

$ echo $?
1

Here’s what happens:

  1. Since $HOME is present and defined its values is echo-ed
  2. $DOME variable is not defined therefore bash prints the “Must be set” message as specified in the substitution.
  3. Along with message bash sets the exit status to 1 to signify errorneous result of the corresponding command execution.

It’s a handy safeguard for rm -rf ${BUILD_DIR:?}/ case as it ensures $BUILD_DIR is non-empty preventing disastrous rm -rf / case

$ VAR="" && echo ${VAR}ok
ok
$ VAR="" && echo ${VAR?}ok
ok
$ VAR="" && echo ${VAR:?}ok
bash: VAR: parameter null or not set
$ VAR="" && echo ${VAR:?is unset}ok
bash: VAR: is unset

Tip 4: Compose

Enable composition by keeping functionality in functions:

#!/bin/bash

say_hello() {
  echo "Hello ${NAME}"
}

print_date() {
  echo "today is `date`"
}

say_hello
print_date

Functions also act as some sort of domain language to help explain when the script does.

Running the above script produces output

$ source test.sh
Hello
today is Tue 19 Jan 2016 17:41:01 EST

Tip 5: Context Aware

Often script is run in different contexts ie: when being developed and when being run as a Cron job on a production server. Both have different requirements and those requirements could be described in corresponding “context” functions.

in following example env_prod and env_test are 2 different contexts corresponding to production and test environments.

#!/bin/bash

say_hello() {
  echo "Hello ${NAME}"
}

print_date() {
  echo "today is `date`"
}

main() {
  say_hello
  print_date
}

env_prod() {
  local NAME=${NAME?"Required"}
  # sets fail early flag
  set -e
}

env_test() {
  # enable debugging output
  set -x
}

Example running:

$ source test.sh && env_prod && main
-bash: NAME: Required

$ NAME=world env_prod && main
Hello world
today is Tue 19 Jan 2016 17:52:48 EST
$ source test.sh && NAME=world env_test && main

+ say_hello
+ echo 'Hello world'
Hello world
+ print_date
++ date
+ echo 'today is Tue 19 Jan 2016 17:54:20 EST'
today is Tue 19 Jan 2016 17:54:20 EST
+ set +x

Thank you!

Read More
Test Driven Devops with Ansible
Immutable by default?!
Comments
read or add one↓