Getopts how to handle missing '-' in command line args.

I'm using getopts to process command line args in a Bash script. The code looks like this:

while getopts ":cfmvhs:t:" option; do

    case $option in

        c)   operationMode="CHECK"
             ;;

        f)   operationMode="FAST"
             ;;

        m)   operationMode="MULTI"
             ;;

        v)   verbose="TRUE"
             ;;

        h)   Display_Usage_Message
             exit "$ExitStatusSuccess"
             ;;

        s)   numMulti=$OPTARG
             ;;

        t)   timeoutSeconds=$OPTARG
             ;;

        \?)  Output_Error_Message "OptionInvalid"
             exit "$ExitStatusErrorSwitchInvalid"
             ;;

        :)   Output_Error_Message "ArgMissing" "$OPTARG"
             exit "$ExitStatusErrorArgMissing"
             ;;

    esac
done

My question is how I should spot options which have no '-' prefix? When a user has omitted the '-' on the command line by a mistake. e.g. "progname -f v"

If there is an invalid option such as '-x', 'x' not being in my getopts ":cfmvhs:t:" string then getopts spots that and my Output_Error_Message "OptionInvalid" code is run. BUT if the user omits the '-' by a mistake and just enters 'x' on the command line or the user omits the '-' of one of the valid options then this is not handled. Using "*)" in addition to "\?)" in the case statement does not sort this out.

How should this situation be handled?

Thanks.

How is it supposed to tell the difference between a missing -, and a filename named 'x' that the user wants to enter? What if the user really does want to enter a file named x? It can't, its ambiguous.

1 Like

That is a very good point. :slight_smile: I didn't think about that as the script does not take a filename as input. Oops.

I suppose that after the getopts loop has finished I could loop through the args shifting off valid args and then see if anything is left behind at the end.

Thanks Corona.

Hi gencon,
As Corona688 said, the getopts utility is used to parse command line options. When the getopts loop finishes, you should shift off the arguments that it processed and the remaining arguments will be operands for your script. If your script doesn't expect any operands, you can figure that out after the shift. The following rearrangement of your code (with missing defines and functions added and a diagnostic is added at the end if any unexpected operands are found after processing options) may help you understand how to use getopts.

#!/bin/bash
ExitStatusErrorUnexpectedOperand=1
ExitStatusErrorArgMissing=2
ExitStatusErrorSwitchInvalid=3
ExitStatusSuccess=0
IAm=${0##*/}

function Display_Usage_Message() {
    printf "Usage:\t%s [-c|-f|-m] [-v] [-s numMulti] [-t seconds]\n\t%s -h\n" \
        "$IAm" "$IAm"
}

function Output_Error_Message() {
    printf "%s" "$IAm" >&2 
    printf ": %s" "$@" >&2
    echo >&2
}

while getopts ":cfmvhs:t:" option; do
    case $option in
        (c) operationMode="CHECK"
            ;;
        (f) operationMode="FAST"
            ;;
        (h) Display_Usage_Message
            exit "$ExitStatusSuccess"
            ;;
        (m) operationMode="MULTI"
            ;;
        (s) numMulti=$OPTARG
            ;;
        (t) timeoutSeconds=$OPTARG
            ;;
        (v) verbose="TRUE"
            ;;
        (\?)Output_Error_Message "-$OPTARG" "Unknown option" 
            exit $ExitStatusErrorSwitchInvalid
            ;;
        (:) Output_Error_Message "-$OPTARG" "Option missing option argument"
            exit $ExitStatusErrorArgMissing
            ;;
    esac
done
shift $((OPTIND - 1))
if [ $# -gt 0 ]; then
    msg="$(printf "%d unexpected operand(s) found" $#)"
    Output_Error_Message "$msg" "$@"
    exit $ExitStatusErrorUnexpectedOperand
fi
echo "operationMode is \"$operationMode\""
echo "verbose is \"$verbose\""
printf "numMulti is \"%s\"\n" "$numMulti"
printf "timeoutSeconds is \"%s\"\n" "$timeoutSeconds"
exit $ExitStatusSuccess

Sample invocations of this script with resulting output are included below. Note: Output written to stderr is shown in blue; output written to stdout is shown in black; and input to the shell is shown in green.

$ myprogram -h
Usage:	myprogram [-c|-f|-m] [-v] [-s numMulti] [-t seconds]
	myprogram -h
$ echo $?
0
$ ./myprogram -fv -s 512 -t30
operationMode is "FAST"
verbose is "TRUE"
numMulti is "512"
timeoutSeconds is "30"
$ echo $?
0
$ ./myprogram -x
myprogram: -x: Unknown option
$ echo $?
3
$ myprogram -m -t
myprogram: -t: Option missing option argument
$ echo $?
2
$ myprogram -v -t 15 operand1 "quoted operand2" operand3
myprogram: 3 unexpected operand(s) found: operand1: quoted operand2: operand3
$ echo $?
1
3 Likes

Don,

What a thoroughly helpful and informative post, thank you so much. It has helped me to understand what's going on and to resolve my issue with ease. You obviously went to great effort to help me and that was very kind of you and it is greatly appreciated. Cheers Godfather. :smiley:

Your example allowed me to resolve the situation by adding the following code immediately below the end of the getopts while loop.

# getopts stops processing args if it encounters an arg without a '-' prefix (unless it is an
# $OPTARG in which case processing continues). Check if any invalid args have been entered by
# working out how many args getopts has actually processed, shifting them off, and if more
# than 0 args remain then an invalid arg must have been entered.

let "numArgsProcessedByGetopts = OPTIND - 1"
shift "$numArgsProcessedByGetopts"
numArgsRemaining="$#"

if [ "$numArgsRemaining" -gt "0" ]; then
    Output_Error_Message "OptionInvalid"
    exit "$ExitStatusErrorSwitchInvalid"
fi

Thanks again.

One quick question... In your case statement you enclosed the patterns inside "()", I have only ever seen (or at least only ever noticed) a final ")". Is there any difference between the 2 styles of notation and, if so, what is the difference?

# e.g. Do "(c)" and "f)" notation differ in any way?

case $option in
    (c)  operationMode="CHECK"
         ;;
    f)   operationMode="FAST"
         ;;
esac

Hi gencon,
I'm glad it helped.

It is a personal preference. In the shell, the meaning of (pattern) and pattern) is exactly the same. I prefer the (pattern) form because I use the vi editor (with the showmatch option set) while editing code, and if I try to match the parentheses surrounding a subshell invocation when the subshell contains a case statement, the parentheses will not match up correctly if I use the pattern) form.

Use which ever form looks better to you or works better for you.
Cheers,
Don

1 Like

Thanks again Don, got it. I think I'll use (pattern) in future cos it looks funkier. :cool:

I haven't used vi since my Solaris 2.x days at university but never fully mastered all the keys.

Cheers.