Library for dealing with command line options for bash. Can automatically generates help/usage messages. Supports required fields, short/long options, default values.

# Options library for bash, v1.6
#
# https://github.com/davvil/shellOptions
#
# Copyright 2009 David Vilar
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# options.bash and options.zsh
#     by David Vilar
#
# This is a small library for handling command line options in bash and zsh. It is
# inspired by the optparse python library. An example usage is
#
#     addOption -n --number required help="Specify some number" dest=myNumber
#     addOption -a --aux flagTrue help="Another option, but without dest"
#     addOption -o help="Option with default" default="a b" dest=otherO
#     parseOptions "$@"
#
# This example defines the options -n/--number, -a/--aux and -o. -n sets the
# variable $myNumber and is required by the script to run. -a sets the $aux
# variable to true when given (it does not require arguments). -o sets the
# variable $otherO, but if not specified on the command line, it defaults to "a b".
# Additionaly the option -h/--help is automatically generated and produces this
# output:
#
#     usage: example.bash [options]
#
#     Options:
#
#        -n,--number                Specify some number
#        -a,--aux                   Another option, but without dest
#        -o                         Option with default
#        -h,--help                  Show this help message
#
# To use the library in your own script, copy it somewhere and include it in your
# script (.bash or .zsh depending on which shell you use):
#
# . options.bash
#
# New options are added with the addOption command. It scans its arguments for
# those beginning with a dash to get the short and long option syntax (one of them
# can be omitted). Additionally you can give these additional commands:
#
# + dest=VAR
#   Sets the destination of the variable. If not given, it defaults to
#   the long option name (see option --aux in the example above)
# + action=COMMAND
#   Calls command if the option is specified in the command line (usually this is
#   used for calling functions in the script, but it is not limited to it)
# + default=VALUE
#   Sets the default value for the option, if not given in the command line
# + required
#   Specifies that the option is required [I know, this is a contradiction of
#   terms ;-)]. The script will abort with an error message if the option is not
#   given in the command line
# + help=MESSAGE
#   Specifies the help message to be written in the output with the -h options
# + flagTrue/flagFalse
#   Specifies that the option is a boolean. flagTrue sets the default value to
#   false and turns it to true if the option is given in the command line. flagFalse
#   works the other way round. Note that you can use such variable directly in bash
#   constructs like if clauses, while loops, etc:
#
#       if $var; then
#               echo "var is set
#       fi
#
# + configFile
#   This option makes the script read an external file. Normally this is used as a
#   config file, where the values of variables are set, e.g.
#
#       input=file.in
#       output=file.out
#       iterations=3
#
#   Note that the file is included directly in bash, no fancy parsing is done.
#   This means among other things that no whitespace is allowed around = signs and
#   that command can be executed from within the "config file". Use with caution.
# + dontShow
#   Do not show the option in the output of -h. Useful if some options are thought
#   to be used only when calling the script from other script.
#
# Once you have defined the options for your script, use the command
#     parseOptions "$@"
# to parse the command line and set the appropriate variables. Additional command
# line arguments are stored in the $optArgv[] array, with $optArgc storing the
# number of actual arguments.
#
# The library follows the usual convention of supporting -- to separate options
# from command line arguments, e.g. in the call
#     ./example.sh -n 3 -a -- -b aaa
# the arguments are "-b" and "aaa".
#
# This library has been extensively used in the Jane statistical machine
# translation toolkit (http://www.hltpr.rwth-aachen.de/jane)
#

#function debug { $* > /dev/stderr; }
function debug { true; }

function setUsage() {
    __programUsage__="$*"
}

declare -a __shortOptions__
declare -a __longOptions__
declare -a __optionDests__
declare -a __optionActions__
declare -a __optionDefaults__
declare -a __optionRequired__
declare -a __optionFlag__
declare -a __optionHelp__
declare -a __dontShow__

declare -a optArgv
optArgc=0
__nOptions__=0;
__programUsage__="usage: `basename $0` [options]\n\nOptions:\n"

function addOption() {
    debug echo "Option #${__nOptions__}:"
    # These arrays must have the same length for the __searchInArray__ function to work
    __longOptions__[$__nOptions__]="___not_a_valid_option___"
    __shortOptions__[$__nOptions__]="___not_a_valid_option___"
    local i
    for i in "$@"; do
        if [[ "${i:0:2}" = -- ]]; then
            __longOptions__[$__nOptions__]=${i:2}
            debug echo -e "\tRegistered long option ${i:2}"

        elif [[ "${i:0:1}" = - ]]; then
            __shortOptions__[$__nOptions__]=${i:1}
            debug echo -e "\tRegistered short option ${i:1}"

        elif [[ "${i:0:5}" = dest= ]]; then
            __optionDests__[$__nOptions__]=${i:5}
            debug echo -e "\tOption dest is ${i:5}"

        elif [[ "${i:0:7}" = action= ]]; then
            __optionActions__[$__nOptions__]=${i:7}
            debug echo -e "\tOption action is ${i:7}"

        elif [[ "${i:0:8}" = default= ]]; then
            __optionDefaults__[$__nOptions__]="${i:8}"
            debug echo -e "\tOption default is ${i:8}"

        elif [[ "$i" = required ]]; then
            __optionRequired__[$__nOptions__]=1
            debug echo -e "\tOption required"

        elif [[ "${i:0:5}" = help= ]]; then
            __optionHelp__[$__nOptions__]=${i:5}
            debug echo -e "\tRegistered help \"${i:5}\""

        elif [[ "$i" = flagTrue ]]; then
            __optionFlag__[$__nOptions__]=1
            debug echo -e "\tOption is flag (true)"

        elif [[ "$i" = flagFalse ]]; then
            __optionFlag__[$__nOptions__]=0
            debug echo -e "\tOption is flag (false)"

        elif [[ "$i" = configFile ]]; then
            __configFile__=$__nOptions__
            debug echo -e "\tOption is a config file"

        elif [[ "$i" = dontShow ]]; then
            __dontShow__[$__nOptions__]=1
            debug echo -e "\tDon't show this option"

        else
            echo "Unknown parameter to registerOption: $i" > /dev/stderr
            exit 1
        fi
    done

    if [[ "${__optionDests__[$__nOptions__]}" = "" && "${__longOptions__[$__nOptions__]}" != "" ]]; then
        __optionDests__[$__nOptions__]=${__longOptions__[$__nOptions__]}
    fi

    ((++__nOptions__))
}

function __searchInArray__() {
    debug echo -e "\t__searchInArray__ $*"
    local searchFor=$1
    shift
    local i=0;
    while [[ "$1" != "" ]]; do
        if [ $1 = $searchFor ]; then
            debug echo -e "\tFound as option $i"
            echo $i
            return
        fi
        ((++i))
        shift
    done
}

function __searchOption__() {
    local pos
    if [[ "${1:0:2}" = -- ]]; then
        debug echo -e "\tIt's a long option"
        pos=`__searchInArray__ ${1:2} ${__longOptions__[*]}`
    elif [[ "${1:0:1}" = -  ]]; then
        debug echo -e "\tIt's a short option"
        pos=`__searchInArray__ ${1:1} ${__shortOptions__[*]}`
    fi
    debug echo -e "\tResulting pos: $pos"
    echo $pos
}

function __readConfig__() {
    if [[ "$__configFile__" != "" ]]; then
        local options=("$@")
        local i=0;
        while (($i < ${#options[*]})); do
            if [[ "${options[$i]}" = -${__shortOptions__[$__configFile__]} ||
                  "${options[$i]}" = "--${__longOptions__[$__configFile__]}" ]]; then
                . ${options[$((i+1))]}
                break
            fi
            if [[ "${options[$i]}" = "--" ]]; then
                break
            fi
            ((++i))
        done
    fi
}

function parseOptions() {
    # Set default values
    local i=0;
    local flag;
    while (($i < $__nOptions__)); do
        default="${__optionDefaults__[$i]}"
        if [[ "$default" != "" ]]; then
            eval ${__optionDests__[$i]}=\"$default\"
        elif [[ "${__optionFlag__[$i]}" != "" ]]; then
            flag=${__optionFlag__[$i]}
            if [[ $flag = 1 ]]; then
                eval ${__optionDests__[$i]}=false
            else
                eval ${__optionDests__[$i]}=true
            fi
        fi
        ((++i))
    done

    # Read a config file (if we have one)
    __readConfig__ "$@"

    # Parse the options
    local minusMinusSeen=false
    while (($# > 0)); do
        if ! $minusMinusSeen; then
            debug echo "Searching option $1"
            if [[ "$1" = "--" ]]; then
                minusMinusSeen=true
            else
                local pos=`__searchOption__ "$1"`
                if [[ "$pos" == "" ]]; then
                    if [[ "${1:0:1}" = "-" ]]; then
                        echo "Error: Unknown option $1" > /dev/stderr
                        exit 1
                    else
                        optArgv[optArgc]="$1"
                        ((++optArgc))
                    fi
                else
                    local dest=${__optionDests__[$pos]}
                    if [[ "$dest" != "" ]]; then
                        debug echo -e "\tOption has a dest"
                        local isFlag=${__optionFlag__[$pos]}
                        if [[ "$isFlag" != "" ]]; then
                            debug echo -e "\tOption $i is a flag"
                            if [[ $isFlag = 1 ]]; then
                                eval $dest=true
                            else
                                eval $dest=false
                            fi

                        else # It's not a flag
                            shift
                            value="$1"
                            eval $dest=\"$value\"
                        fi
                    fi

                    local action=${__optionActions__[$pos]}
                    if [[ "$action" != "" ]]; then
                        debug echo -e "\tOption has an Action"
                        eval ${__optionActions__[$pos]}
                    fi
                fi
            fi
        else # minusMinusSeen
            optArgv[optArgc]="$1"
            ((++optArgc))
        fi

        shift
    done

    # Check for required values
    i=0
    local -a missingOptions
    while (($i < $__nOptions__)); do
        debug echo "Checking option $i, required=\"${__optionRequired__[$i]}\", destValue=${!__optionDests__[$i]}"
        if [[ "${__optionRequired__[$i]}" != "" && "${!__optionDests__[$i]}" = "" ]]; then
            debug echo -e "\tMissing option"
            local optionName=${__shortOptions__[$i]}
            if [[ "$optionName" = "___not_a_valid_option___" ]]; then
                optionName="--"${__longOptions__[$i]}
            else
                optionName="-"$optionName
            fi
            missingOptions[${#missingOptions[*]}]=$optionName
        fi
        ((++i))
    done
    if (( ${#missingOptions[*]} > 0 )); then
        echo -n "Error: following mandatory options are missing: ${missingOptions[0]}" > /dev/stderr
        i=1
        while (($i < ${#missingOptions[*]})); do
            echo -n ",${missingOptions[$i]}" > /dev/stderr
            ((++i))
        done
        echo "" > /dev/stderr
        exit 1
    fi
}

function __printOptionHelp__() {
    local i=$1
    if [[ "${__dontShow__[$i]}" = 1 ]]; then
        return
    fi
    local optionId=${__shortOptions__[$i]}
    if [[ $optionId != ___not_a_valid_option___ ]]; then
        optionId=-$optionId
        if [[ ${__longOptions__[$i]} != ___not_a_valid_option___ ]]; then
            optionId=$optionId","
        fi
    else
        optionId=""
    fi
    if [[ ${__longOptions__[$i]} != ___not_a_valid_option___ ]]; then
        optionId=${optionId}"--"${__longOptions__[$i]}
    fi
    optionId="   "$optionId
    local firstIndentLine
    if ((${#optionId} < 30)); then
        echo -n "$optionId"
        local c
        for c in `seq ${#optionId} 29`; do
            echo -n " "
        done
        firstIndentLine=2
    else
        echo "$optionId"
        firstIndentLine=1
    fi
    local thirtySpaces="                              "
    echo "${__optionHelp__[$i]}" | fmt -w 50 | sed "$firstIndentLine,\$s/^/$thirtySpaces/"
}

function __showHelp__() {
    echo -e "$__programUsage__"
    local i=1 # -h is the first option
    while (($i < $__nOptions__)); do
        __printOptionHelp__ $i
        ((++i))
    done
    __printOptionHelp__ 0 # -h
    exit 0
}

addOption -h --help action=__showHelp__ help="Show this help message"