Tags


My bash template for option parsing

First written on November 18, 2018
Last updated on August 11, 2019

Hello again,

in the past years I have written a lot of scripts which rely on getopt, a program that parses command line options (both short and long) and arguments.

Getopt is part of the util-linux package. Please, don’t confuse getopt with getopts ; they are quite different. See this and this for a comparison between the two.

Please note that the script and the documentation reported here, except for the introduction and the “Reason” paragraph, is outdated. For this reason you should refer to the repository.

Please note that the documentation and source code reported here are old. Refer to the fbopt repository.

Reason

To avoid reinventing the wheel and confusion, I have written some notes and a dummy script that will serve as template. The script is called fbopt, i.e: Franco Bash Option Parsing Template. The purpose of this is to change the current implementations of the option parsers already present in some scripts I have written, as well as use it for new ones.

Usage

You can for example adapt and include the template as a separate part of your program by using source ./fbopt or . ./fbopt from the main script.

Bashisms

Bashisms are Bash specific syntax elements. You will find at least the following bashisms in the template

BashismExample
local variableslocal variable='value'
arrays"${array[@]}"
indirect variables"${!variable}"

Conventions

These elements should be common sense and not be specific to fbopt

ConventionExampleNotes
constants are enclosed within single quotes'constant'elements within single quotes (only) are not interpreted by the shell
variables enclosed within double quotes"${variable}"double quotes serve as a delimiter between multiple variable names if these are consecutive. Every variable between the quotes is interpolated
variables use the curly braces notation"${variable}"curly braces serve as a delimiter between multiple variable names if these are consecutive
variables not enclosed within double quotes${?} or for v in ${values}; do echo "${v}"; donethe only variables allowed without double quotes are integers (such as return values) and loop iterators (because these won’t work otherwise)

Dependencies and compliancy

The template is known to work with the following packages

PackageVersion command
GNU Bash$ bash --version
getopt$ getopt --version

fbopt compatibility:

PackagePackage versionfbopt version
GNU BashGNU bash, version 4.4.23(1)-release (x86_64-unknown-linux-gnu)0.1, 0.2, 0.3
getoptgetopt from util-linux 2.330.1, 0.2, 0.3

The template

To allow inclusion in any project the template is released under the CC0 1.0 license.

What follows is fbopt version 0.3.

#!/bin/bash
#
# fbopt version 0.3
#
# Written in 2018 by Franco Masotti/frnmst <franco.masotti@student.unife.it>
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the public
# domain worldwide. This software is distributed without any warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication along
# with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
#
#
# See also https://frnmst.gitlab.io/notes/my-bash-option-parsing-template.html
#

show_help()
{
    cat <<-EOF
Usage: <program_name> [OPTION] ARGUMENT
<Description>

Mandatory arguments to long options are mandatory for short options too.
Options:
    -a, --flag-a=FLAGA          set a
    -b, --flag-b=FLAGB          set b
    -c                          enable c
    -d                          enable d
    -e, --flag-e=FLAGE          set e
    -f[=FLAGF]                  enable and or set f
                                with an optional argument
    -h, --help                  print this help
    --print-flags               print the enabled options. This can also
                                be used to print the default options

Exit status:
 0  if OK,
 1  if an error occurred.

<License header>
<Copyright>
EOF
}

# A function that prints the variable name and value of all
# the flags enabled by the user. This is useful to check that
# all the flags are correct, as kind of a dry run.
show_flags()
{
    local flags="${*}"

    for flag in ${flags}; do
            printf "%s='%s'\n" "${flag}" "${!flag}"
    done
}

getopt_error()
{
    local program_name="${0}"

    printf "%s\n" "Try '"${program_name}" --help' for more information"
} 1>&2 2>&-

main()
{
    # Create a new array from the reference of the input one.
    # See https://stackoverflow.com/questions/1063347/passing-arrays-as-parameters-in-bash
    # See some comments below.
    declare -a argc=("${!1}")

    # Set the options. flag_f has an optional argument.
    local getopt_short_options='a:b:c:def::hi'
    local getopt_long_options='flag-a:,flag-b:,flag-e,print-flags,help,'

    # Set the default values for the flags.
    local flag_a=''
    local flag_b=''
    local flag_c=''
    local flag_d='false'
    local flag_e='false'
    local flag_f='false'

    local program_name="${0}"
    opts="$(getopt \
        --name "${program_name}" \
        --shell bash \
        --options "${getopt_short_options}" \
        --longoptions "${getopt_long_options}" \
        -- \
        "${argc[@]}")"
    getopt_retval=${?}
    # Check that getopt works and that some kind of argument
    # is passed to the script. This is "quotation hell".
    a="'"${argc[@]}"'"
    { [ ${getopt_retval} -ne 0 ] || [ -z "${a}" ]; } && getopt_error && return 1
    eval set -- "${opts}"

    # Option parsing.
    while [ "${1}" != '--' ]; do
        case "${1}" in

            -a | --flag-a ) flag_a="${2}";
                            shift 1 ;;

            --flag-b )      flag_b="${2}";
                            shift 1 ;;

            -c )            flag_c="${2}";
                            shift 1 ;;

            -d )            flag_d='true' ;;

            -e | --flag-e ) flag_e='true' ;;

            -f )            flag_f="${2}";
                            shift 1 ;;

            -h | --help )   help='true' ;;

            --print-flags ) print_flags='true' ;;

        esac
        # Iterate through all arguments.
        shift 1
    done

    shift 1
    # Everything else after '--' is an argument.
    argc="${*}"

    # Check that the flags that must be non empty are actually not empty.
    # A user might infact circumvent getopt's mechanisms like this
    # ./program -flag ''
    # This can also be done inside the option parser loop but to avoid nestings
    # I prefer it done here.
    { [ -z "${flag_a}" ] \
        || [ -z "${flag_b}" ]; } \
        && getopt_error && return 1
    [ "${print_flags}" = 'true' ] \
        && show_flags \
            'flag_a' \
            'flag_b' \
            'flag_c' \
            'flag_d' \
            'flag_e' \
            'flag_f' \
        && return 0
    [ "${help}" = 'true' ] && show_help && return 0
    # Override values of optional parameters.
    [ -z "${flag_f}" ] && flag_f='true'

    # From now on you should call a function or an external program
    # using the values of the flag variables.
    [ "${flag_a}" = 'alpha' ] && do_something_alpha && return ${?}
    [ "${flag_a}" = 'a' ] && do_something_a && return ${?}
}

# Test dependencies and versions.
# getopt must return 4 to be fully compatible. See getopt's manual.
which bash getopt 1>/dev/null 2>/dev/null && { getopt -T; [ ${?} -eq 4 ]; }

# Get and pass argc to the main function.
# All this work with an array must be done to preserve
# quotations for arguments that have whitespaces.
# See https://lists.debian.org/debian-user/2007/12/msg01244.html
declare -a opts=()
for opt in "${@}"; do
    opts=("${opts[@]}" "${opt}")
done
main 'opts[@]'

~

Enjoy :)


Post a new comment