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 printf
s 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:
- Since
$HOME
is present and defined its values isecho
-ed $DOME
variable is not defined therefore bash prints the “Must be set” message as specified in the substitution.- 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!