Tuesday, January 26, 2016

Make better bash scripts for yourself (and others)

When you write a bash script, there's a few things you can do to make your life (and anyone else who uses it) a lot better. Here's a few of the things that I (try) to do when I write scripts to make it easier to use and less error prone.

#1 Comments

Seriously. Write them. A lot. Explain what you're doing so that when you come back 6 months later to see what the 'makestuff.sh' script does, at least you can figure out what you were doing from the comments as well as the commands.

#2 Filenames

Don't name your scripts 'makestuff.sh'. Even if you think you're only going to use it once, name it something good. Same goes for variables in the script.

#3 USAGE="..."

Always have a USAGE variable and echo it out when the script is called with -h. If your script requires arguments, print it when none are supplied. The bonus is, it's like a comment for the whole script. Put it at the top and when you open it later to see what it does, you'll smile knowing you could have just used the -h flag. It's also handy if you always forget what options and arguments you need.

Here's the simplest way to do it:

#!/bin/bash

USAGE="
$(basename $0) usage:

    $(basename $0) arg1 [arg2 ...]

    Basic description of script functionality goes here
"

if [ "$1" == "-h" ]; then
    echo "$USAGE"
    exit
fi
# rest of the script goes here

If your script needs a minimum number of arguments, why not print it when too few are supplied?

if [ $# -lt 1 ]; then
    >&2 echo "$USAGE"  # echo to stderr
    exit 1  # exit with error code
fi

See below for how to do this with option parsing

#4 `set -u' and `set -e'

David Pashley has a great article called Writing Robust Bash Scripts where he outlines many good practices when writing scripts. Using set -u and set -e are just two of the many great suggestions he has.

set -u tells the shell to abort if you try to use a variable that wasn't set. Seems kinda drastic, but this will save you a lot of headaches when an unset variable is evaluating empty just because you spelled it wrong or forgot to initialize it.

set -e tells the shell to abort if any command fails. I like this one especially for scripts that do a lot or rming or other dangerous stuff. If something breaks before the script gets to the dangerous part, I want it to stop.. like now!

#5 Option parsing

Okay, so now we're getting into the fun stuff.. Option parsing is not all that difficult in bash and I didn't do it for a long time. Until I came across this stackoverflow answer.

Here's a generalized version of the answer in the link:

#!/bin/bash
# see http://stackoverflow.com/a/14203146 for more on this
set -e
set -u

###################
# Option Defailts #
###################
opt="option default"
flag=false
verbose=0

###################
# Version & Usage #
###################
VERSION="0.0.0"
USAGE="
$(basename $0) version $VERSION

usage:

    $(basename $0) [options] arg1 [arg2 ...]

options:
    -o, --opt val   set opt to val [$opt]
    -f, --flag      enable flag
    -v, --verbose   increase verbosity
    -h, --help      print this message and exit
    --              options terminator

    DESCRIPTION
"

#################
# Parse Options #
#################
while [[ $1 == -* ]]; do
    case $1 in
        -o|--opt)
            opt="$2"
            shift  # past value
            ;;
        -f|--flag)
            flag=true
            # do not shift again!
            ;;
        -v|--verbose)
            ((verbose+=1))
            # do not shift again!
            ;;
        -h|--help)
            echo "$USAGE"
            exit
            ;;
        --)
            shift
            break
            ;;
        *)
            >&2 echo "$(basename $0): unknown option $1" \
                "(run '$(basename $0) --help' for available options)"
            exit 1
    esac
    shift
done
# all options have been shifted out, so just arguments are left in $@

###############
# Main Script #
###############
echo "opt = $opt"
if [ "$flag" = true ]; then
    echo "flag = enabled ($flag)"
else
    echo "flag = disabled ($flag)"
fi
echo "verbose = $verbose"
i=0
for arg in "$@"; do
    echo "arg $i = $arg"
    ((i+=1))
done

Additionally, you can define some helper functions to echo commands given the verbosity level:

error() { >&2 echo $*; }                          # always prints to stderr
warn() { (($verbose>0)) && echo $* || true; }     # prints when verb is >0
info() { (($verbose>1)) && echo $* || true; }     # prints when verb is >1
debug() { (($verbose>2)) && echo $* || true; }    # prints when verb is >2
verbose() { (($verbose>3)) && echo $* || true; }  # prints when verb is >3

No comments :