Bash Script Hygiene

Today I want to share a companion to the Batch Script Hygiene and PowerShell Script Hygiene posts from a while back.

Bash is one of the most commonly used shells, so it's worth spending a bit of time gaining some fluency and learning how to write good, maintainable scripts.

For general conventions, - Shell Style Guide from Google

Isolating changes

In bash, changes to environment variables and such will not propagate to the caller by default.

If you do want that, however, you can use source or the dot operator.

Error handling

There are two main approaches to handling errors.

  1. Set the option to have errors checked automatially and exit on non-zero values. This is done via invoing setopt with the -e switch, paired with the -o pipefail so it works with the last command of pipelines too.
  2. Check the errors manually.

Now, if you're going to check errors manually, again there are a couple of approaches.

  1. Run and test in a single line. You can do this with if conditionals
  2. Run the command and capture the exit code.

The exit code from a program will be stored in the $? variable.

Be careful because $? will get overwritten as soon as you run a different program, so you'll want to test it 'quickly' or save it somewhere.

If you're doing things manually, you might want to try alternate paths depending on the error code or something of the sort. If you do decide to bail out with an error, you can use the exit builtin or return if you are in a function or in a sourced script.

Finally, if you want to jump to a cleanup section, you can use the trap command, but because Bash supports functions, you often don't have to resort to that.

#!/bin/bash

function cleanup() {
  echo cleaning up ...
}

function run_my_command() {
  echo do something ...
  return 1 # oh no!
}

run_my_command
if (( $? != 0 )); then
  cleanup
  exit 1
)

echo success!
exit 0

Argument handling

To handle arguments to your batch file, you will typically want to start by keeping them simple. Bash may be more powerful than Windows batch files, but not by a lot.

Arguments come in as variables in the form $n, where n=0 is the batch itself, and n=1 is the first argument. If you refer to non-existent variables, they show up as blank strings.

If you can require positional parameters, this might be all you need. You can test for the presence of the parameters with a simple if [[ -z $5 ]]; then echo missing argument; exit; fi

If you instead have variable arguments, you can often process them in a loop by refering to the first variable one and shifting the parameters into that position. This is similar to the approach presented in the Windows batch scripting post a while back.

The How do I parse command line arguments in Bash? StackOverflow question has quite a bit of detail on the various approaches availabe.

Logging

There are two approaches to log from batch files - an explicit log file you write to, or simply writing to output.

In both cases, you can use echo, but printf can provide much richer output.

To write to stderr instead of stdout, you use: echo hello >&2

To redirect an program's stderr along with its stdout, use: command > file 2>&1

To log to the system log, see logger.

Unlike batch files, statements are not printed as they are evaluated, but you can use set -x to expand variables and print statements out (or just set -v to skip the expansion). You can run this with bash -vx THE-SCRIPT to avoid having to modify the script. There are a bunch of additional tips on debugging that are worth checking.

Composing

Once you start building more interesting things, you'll want to reuse bits of code, or organize it in blocks.

Thankfully, Bash has decent support for simple functions.

You can combine this with source or the dot operator to build libraries of functions and then include them in your script.

You can run shell scripts as programs, of course, which also lets you look at exit codes to understand what happened. And finally, you can assign the stdout content to a variable via command substitution, or just use it directly of course. The $() syntax is preferred overbackticks.

OTHER_OUTPUT=$(./other-script.sh)
echo foo is ${OTHER_OUTPUT}

Note how this is different from ${}, which is regular old parameter or variable expansion.

Resources

Here is a list of useful resources for bash scripting.

Happy scripting!

Tags:  shell

Home