I have long referred to Sharat’s Shell Script Best Practices as a starting point for my scripts, but I was working on an email processing script and ended up making a few tweaks I thought constituted good defaults. That article is still great explanation of the motivation behind this template, which I won’t repeat. Here’s my new template in case it’s useful to anyone else:

#!/usr/bin/env bash
 
set -o errexit
set -o nounset
set -o pipefail
 
USAGE="Usage: $0 [options] PARAM1
 
DESCRIPTION
    This program does something!
 
OPTIONS
    --opt1 (type)
    Description."
 
if [[ "${TRACE-0}" == "1" ]]; then
    set -o xtrace
fi
 
if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then
    echo "$USAGE"
    exit
fi
 
cleanup() {
}
 
trap cleanup EXIT
 
main() {
    local PARAM1=""
    local OPT1=""
 
    while [[ $#--gt-0-| -gt 0 ]]; do
        case "$1" in
            --opt1)
                shift
                if [[ $#--eq-0-| -eq 0 ]]; then
                    echo "Error: --opt1 requires an argument."
                    echo "$USAGE"
                    exit 1
                fi
                OPT1="$1"
                ;;
            -*)
                echo "Unknown option: $1"
                echo "$USAGE"
                exit 1
                ;;
            *)
                PARAM1="$1"
                ;;
        esac
        shift
    done
 
    if [ -z "$PARAM1" ]; then
        echo "Error: PARAM1 is a required parameter."
        echo "$USAGE"
        exit 1
    fi
 
    if [ -z "$OPT1" ]; then
        echo "Error: --opt1 is a required option."
        echo "$USAGE"
        exit 1
    fi
 
	# code here
}
 
main "$@"