Building a Better Bash Script

I often find myself writing a lot of bash scripts that wrap functionality of my services and programs in ways that make my job easier. While bash doesn’t lend itself well to traditional “inheriting” of program elements, it still is very helpful to build a toolbox of snippets, templates, and bits of script that let me assemble new scripts with tried and true methods to get things done.

One of the hardest things to do well in bash is parsing command line options in a consistent cross platform way. The template below shows an example of how to do this that allows for short, long and positional options.

The template uses getopt to normalize short options after parsing long options in a two pass approach. Using getopt transforms options into their canonical form – for example, the option string “-c foobar -laht -v” after being parsed by getopt, becomes “-c foobar -l -a -h -t -v”, allowing them to be handled more consistently.

The getopt string (stored in the variable “opts” below) consists of all the short option letters, with letters that take an argument (e.g. cmd -a foo) being followed by a colon. So if I had short options “a” and “b”, and “b” took an argument, my getopt string would be “ab:”.

# Option defaults
OPT="value"

# Gets the command name without path
cmd(){ echo `basename $0`; }

# Help command output
usage(){
echo "\
`cmd` [OPTION...]
-f, --flag; Set a flag
-o, --opt; Set an option with argument (default: $OPT)
-v, --verbose; Enable verbose output (include multiple times for more
             ; verbosity, e.g. -vvv)
" | column -t -s ";"
}

# Error message
error(){
    echo "`cmd`: invalid option -- '$1'";
    echo "Try '`cmd` -h' for more information.";
    exit 1;
}

# getopt string
opts="fvo:"

# There's two passes here. The first pass handles the long options and
# any short option that is already in canonical form. The second pass
# uses `getopt` to canonicalize any remaining short options and handle
# them
for pass in 1 2; do
    while [ -n "$1" ]; do
        case $1 in
            --) shift; break;;
            -*) case $1 in
                -f|--flag)     FLAG=1;;
                -o|--opt)      OPT=$2; shift;;
                -v|--verbose)  VERBOSE=$(($VERBOSE + 1));;
                --*)           error $1;;
                -*)            if [ $pass -eq 1 ]; then ARGS="$ARGS $1";
                               else error $1; fi;;
                esac;;
            *)  if [ $pass -eq 1 ]; then ARGS="$ARGS $1";
                else error $1; fi;;
        esac
        shift
    done
    if [ $pass -eq 1 ]; then ARGS=`getopt $opts $ARGS`
        if [ $? != 0 ]; then usage; exit 2; fi; set -- $ARGS
    fi
done

# Handle positional arguments
if [ -n "$*" ]; then
    echo "`cmd`: Extra arguments -- $*"
    echo "Try '`cmd` -h' for more information."
    exit 1
fi

# Set verbosity
if [ "0$VERBOSE" -eq 0 ]; then
    # Default, quiet
elif [ $VERBOSE -eq 1 ]; then
    # Enable log messages
elif [ $VERBOSE -ge 2 ]; then
    # Enable high verbosity
elif [ $VERBOSE -ge 3 ]; then
    # Enable debug verbosity
fi

The other interesting thing is that this template allows for multiple verbose flags (-v, -vv, -vvv, etc.) to specify different levels of verbosity – which can be a very handy thing for debugging a misbehaving script or controlling output from a subcommand.

Have other good tips for bash scripting? Share them in the comments!

View this template as a gist on github: https://gist.github.com/2765260

Jake Alheid

Jake is a Python evangelist and is a developer at about.me in San Francisco. He is also the creator of pyconfig and a code contributor on github.