2

I've been learning command line argument parsing. There are long threads about this already, I don't mean to provoke one here:

Using getopts in bash shell script to get long and short command line options

How do I parse command line arguments in Bash?

Using getopts, if you want to parse an argument/value pair like "--opt value", one way is to let getopts treat this as an argument named "-" and the value becomes "-opt". Then we parse that and take the user value by the symbol ${!OPTIND}. I need to know more about it.

In the first thread I cited above, ${!OPTIND} was used, somebody said "what's that?" and answer was "its an indirect substitution". After reading notes on indirect references, especially https://unix.stackexchange.com/questions/41292/variable-substitution-with-an-exclamation-mark-in-bash and http://mywiki.wooledge.org/BashFAQ/006, I generally understand indirection, but I do not understand ${!OPTIND} as an example of it.

The value of $OPTIND is an integer, index of next command line argument. It is not a value in another array.

In the BashFAQ/006 link above, there are warnings about indirection and general advice not to use it. Maybe it is no big deal, but I'd like to avoid danger where possible.

Could we avoid indirection? Seems like I ought to be able to just use ${OPTIND} as an integer to take a value from $@, $@[$OPTIND]}.

In case you want example, here is a script I called "cli-6.sh" and it will receive long form arguments WITHOUT equal signs. Run like this:

$ ./cli-6.sh -v --fred good --barney bad --wilma happy

Leave off -v for less verbosity.

$ ./cli-6.sh --fred good --barney bad --wilma happy

After Parsing values, ordinary getopts

VERBOSE  0
Arrays of opts and values
optary:  fred barney wilma
valary:  good bad happy

Hopefully, this runs for you too :) I did not use an associative array to hold the values because I had hope this would work in other shells, eventually.

#/usr/bin/env bash

die() {
    printf '%s\n' "$1" >&2
    exit 1
}


printparse(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
    fi
}

showme(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'VERBOSE: %s\n' "$1" >&2;
    fi
}


VERBOSE=0

## index for imported values in optary and valary arrays
idx=0
## Need v first so VERBOSE is set early
optspec=":vh-:"
while getopts "$optspec" OPTCHAR; do
    case "${OPTCHAR}" in
        -)
            showme "OPTARG:  ${OPTARG[*]}"
            showme "OPTIND:  ${OPTIND[*]}"
            showme "OPTCHAR: ${OPTCHAR}"
            showme "There is no equal sign in ${OPTARG}"
            opt=${OPTARG}
            val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
            printparse "--${OPTARG}" " " "\"${val}\""
            if [[ "$val" == -* ]]; then
                die "ERROR: $opt value must be supplied"
            fi
            optary[${idx}]=${opt}
            valary[${idx}]=${val}
            idx=$(($idx + 1))
            ;;
        h)
            echo "usage: $0 [-v] [--anyOptYouQant[=]<valueIsRequired>] [--another[=]<value>]" >&2
            exit 2
            ;;
        v)
            ## if -v flag is present, it means TRUE
             VERBOSE=1
             ;;
        *)
             if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                 die "Undefined argument: '-${OPTARG}'"
             fi
             ;;
    esac
done

echo "After Parsing values, ordinary getopts"
echo "VERBOSE  $VERBOSE" 

echo 'Arrays of opts and values'
echo "optary:  ${optary[*]}"
echo "valary:  ${valary[*]}"
pauljohn32
  • 2,079
  • 21
  • 28
  • 1
    Because it's an integer, it's directing you to one of the positional parameters, $1, $2, ... – glenn jackman Oct 18 '17 at 16:13
  • It seems like a very perverse hack to use getopts to parse long options. I'd use [`getopt`](http://manpages.ubuntu.com/manpages/xenial/en/man1/getopt.1.html) instead ([example usage](https://git.launchpad.net/~usd-import-team/ubuntu/+source/util-linux/tree/misc-utils/getopt-parse.bash)) – glenn jackman Oct 18 '17 at 16:20
  • getopt (from util-linux) is not available on some platforms, so we choose not to rely on it. – pauljohn32 Oct 18 '17 at 16:55

1 Answers1

0

Not sure if this helps but here is bash only version of an CLI options parser that does not use getopts but accepts short and long arguments. It also handles groups of short form arguments. This should be useful on systems that do not support a recent version of getopts.

#!/bin/bash
#
# Copyright (c) 2017 by Joe Linoff
# MIT Open Source License.
#
# This script shows how to implement an argument parser with
# 4 options. Two of the options are simply flags, one of
# of them has a single argument and the other has 2 arguments.
#
# It is meant to show bash can support reasonably complex
# argument parsing idioms that will make shell scripts
# more user friendly without using getopts. It is useful
# for cases where getopts is not available.
#
# The options demonstrated are:
#
#    1. -h or --help
#    2. -v or --verbose
#    3. -f ARG or --file ARG or --file=ARG
#    4. -c ARG1 ARG2 or --compare ARG1 ARG2
#
# The options parsing allows the following specifications.
#
#    1. -h
#    2. --help
#    3. -v
#    4. --verbose
#    5. -vv
#    6. -f ARG1
#    7. --file ARG1
#    8. --file=ARG1
#    9. -c ARG1 ARG2
#   10. --compare ARG1 ARG2
#
# This example does not show how to implement best match which would
# mean accepting an option like "--com" (because it is the best unique
# match to --compare). That could be added but i am not convinced
# that it is worth the overhead.
#
# The options parser is global in this example because it is setting
# global (script wide) variables.

# ========================================================================
# Functions
# ========================================================================
# Simple error function that prints the line number of the caller and
# highlights the message in red.
function _err() {
    echo -e "\033[31;1mERROR:\033[0;31m:${BASH_LINENO[0]} $*\033[0m"
    exit 1
}

# ========================================================================
# Main
# ========================================================================
CARGS=()
FILE=''
HELP=0
VERBOSE=0

# The OPT_CACHE is to cache short form options.
OPT_CACHE=()
while (( $# )) || (( ${#OPT_CACHE[@]} )) ; do
    if (( ${#OPT_CACHE[@]} > 0 )) ; then
        OPT="${OPT_CACHE[0]}"
        if (( ${#OPT_CACHE[@]} > 1 )) ; then
            OPT_CACHE=(${OPT_CACHE[@]:1})
        else
            OPT_CACHE=()
        fi
    else
        OPT="$1"
        shift
    fi
    case "$OPT" in
        # Handle the case of multiple short arguments in a single
        # string:
        #  -abc ==> -a -b -c
        -[!-][a-zA-Z0-9\-_]*)
            for (( i=1; i<${#OPT}; i++ )) ; do
                # Note that the leading dash is added here.
                CHAR=${OPT:$i:1}
                OPT_CACHE+=("-$CHAR")
            done
            ;;
        -h|--help)
            (( HELP++ ))
            ;;
        -v|--verbose)
            # Increase the verbosity.
            # Can accept: -v -v OR -vv.
            (( VERBOSE++ ))
            ;;
        -f|--file|--file=*)
            # Can be specified multiple times but we only accept the
            # last one.
            # Can accept: --file foo and --file=foo
            if [ -z "${OPT##*=*}" ] ; then
                FILE="${OPT#*=}"
            else
                FILE="$1"
                shift
            fi
            [[ -z "$FILE" ]] && _err "Missing argument for '$OPT'."
            ;;
        -c|--compare)
            # Can be specified multiple times but we only accept the
            # last one.
            # Can accept:
            #   --compare ARG1 ARG2
            # Cannot accept:
            #   --compare=*
            # The reason for not accepting the '=' sign is to reduce
            # complexity because of the ambiguity of separators. If
            # you decide that you will always use a comma as the
            # separator, that is fine until one of the arguments
            # contains a comma.
            CARG1="$1"
            CARG2="$2"
            shift 2
            [[ -z "$CARG1" ]] &&  _err "Missing both arguments for '$OPT'."
            [[ -z "$CARG2" ]] &&  _err "Missing second argument for '$OPT'."
            CARGS=()
            CARGS+=("$CARG1")
            CARGS+=("$CARG2")
            ;;
        -*)
            _err "Unrecognized option '$OPT'."
            ;;
        *)
            _err "Unrecognized argument '$OPT'."
            ;;
    esac
done

echo "COMPARE : ${CARGS[@]}"
echo "FILE    : ${FILE}"
echo "HELP    : ${HELP}"
echo "VERBOSE : ${VERBOSE}"

The code is also available from https://gist.github.com/jlinoff/1876972c0b37259c82367d51c8313171.

Joe Linoff
  • 761
  • 9
  • 13