5

I've the following simple code:

#!/usr/bin/env bash
while getopts :f arg; do
  case $arg in
    f) echo Option $arg specified. ;;
    *) echo Unknown option: $OPTARG. ;;
  esac
done

and it works in simple scenarios such as:

$ ./test.sh -f
Option f specified.
$ ./test.sh -a -f
Unknown option: a.
Option f specified.

However it doesn't work for the following:

$ ./test.sh foo -f

$ ./test.sh -a abc -f
Unknown option: a.

How do I fix above code example to support invalid arguments?

miken32
  • 42,008
  • 16
  • 111
  • 154
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • What do you mean "to support invalid arguments"? `getopts` stops parsing at the first non-option (e.g. `foo` or `abc`). What are you trying to get it to do? Not do that and parse everything? – Etan Reisner Dec 30 '15 at 20:09
  • To support invalid arguments in terms to recognize that `-f` has been despite specifying unknown arguments, so `-a abc -f` would still recognize `-f`, but it seems `getopts ` is stopping the processing once invalid argument is found. – kenorb Dec 30 '15 at 20:12
  • It stops at the first non-option argument (i.e. `abc`). `-a -a -a -a -f` will print unknown four times for example. – Etan Reisner Dec 30 '15 at 20:18
  • @EtanReisner In my opinion it's pretty weird it stops. I actually needed that for my more extended script which involves calling `getopts` multiple times (but I didn't want to make it complex than it is). So first call deals with all known options, further calls checks only specific parameters, but they fail, because they not recognizing the previous parameters (which I don't care at further calls). – kenorb Dec 30 '15 at 20:21
  • Again, unknown options aren't the problem. Non-option arguments (arguments that don't start with `-`) are the "problem". Stopping on the first non-option argument is POSIX specified behavior. This is only a problem for you because you are parsing the same arguments looking for different valid options the way you are. If you just tell every getopts call about every argument (but only actually **do** anything with the arguments you care about) you shouldn't have a problem. – Etan Reisner Dec 30 '15 at 20:33
  • Yes, probably specifying the all options and keep them in the variable and re-using the same list over and over again would be a better solution. – kenorb Dec 30 '15 at 20:39
  • Yeah, I'd either parse them all at once or do that. Remember you don't need to actually *handle* any of the other options in the `case` statement, you can just ignore them entirely and only handle the ones you care about. – Etan Reisner Dec 30 '15 at 20:52
  • I've just came across this behaviour. I think it must be classified as a bug in getopts, and potentially a dangerous one. Perhaps you could file a bug report? – Łukasz Grabowski Dec 18 '16 at 00:24

3 Answers3

5

It seems getopts simply exits loop once some unknown non-option argument (abc) is found.

I've found the following workaround by wrapping getopts loop into another loop:

#!/usr/bin/env bash
while :; do
  while getopts :f arg; do
    case $arg in
      f)
        echo Option $arg specified.
        ;;
      *)
        echo Unknown option: $OPTARG.
        ;;
    esac
  done
  ((OPTIND++)) 
  [ $OPTIND -gt $# ] && break
done

Then skip the invalid arguments and break the loop when maximum arguments is reached.

Output:

$ ./test.sh abc -f
Option f specified.
$ ./test.sh -a abc -f
Unknown option: a.
Option f specified.
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • `-a` isn't the problem. A **non-option** value is. Drop `foo` and `abc` from the examples and they work (as in your original examples). This is how `getopts` functions. – Etan Reisner Dec 30 '15 at 20:15
  • Do you actually *care* about the `abc` value in these examples? Because this double-loop is going to mean you have to manually walk the arguments *again* and manually find it (by emulating `getopts` logic yourself) if you do. – Etan Reisner Dec 30 '15 at 20:17
  • @EtanReisner I don't care about unknown arguments such as `abc` at that time, as I can read it again at different place of the script. – kenorb Dec 30 '15 at 20:25
3

This topic helped get me to the answer that I was looking for when attempting to detect if specific parameters were present on the command line. I implemented it in a different way, so thought I would share my solution. Comments are included in the code which will hopefully help with understanding this implementation. Some commented out lines also included for debugging purposes.

###############################################################################
#
# Convenience method to test if a command line option is present.
#
# Parameters:
#   $1 - command line argument to match against
#   $2 - command line parameters
#
# Example:
#   is_cmd_line_option_present "v" "$@" 
#       check if the -v option has been provided on the command line
#
###############################################################################
function is_cmd_line_option_present()
{
    _iclop_option="$1"
    # remove $1 from the arguments (via the shift command) to this method before searching for it from the actual command line
    shift
    # Default the return value to zero
    _iclop_return=0

    # Don't need to increment OPTIND each time, as the getopts call does that for us
    for (( OPTIND=1; OPTIND <= $#; ))
    do
        # Use getopts to parse each command line argument, and test for a match
        if getopts ":${_iclop_option}" _iclop_option_var; then
            if [ "${_iclop_option_var}" == "${_iclop_option}" ]; then
                _iclop_return=1
             # else
                # (>&2 echo -e "[Std Err]: is_cmd_line_option_present - Option discarded _iclop_option_var: [${_iclop_option_var}]")
            fi
         else
            # (>&2 echo -e "[Std Err]: is_cmd_line_option_present - Unknown Option Parameter _iclop_option_var: [${_iclop_option_var}]")
            # Need to increment the option indicator when an option is found that isn't listed as an expected option, as getopts won't do this for us.
            ((OPTIND++))
        fi 
    done

    # (>&2 echo -e "[Std Err]: is_cmd_line_option_present end - _iclop_return: [${_iclop_return}]")
    return $_iclop_return;
}
Jokey
  • 31
  • 3
1

The following is a very non-universal workaround with its own issues, but it works at least in my own use case. I suspect OP's question presents a minimal example meant to showcase the problem, so it's likely not to be applicable to the "real" problem.

params=0
 while getopts :f arg; do
  params=1
  case $arg in
    f) echo Option $arg specified. ;;
    *) echo Unknown option: $OPTARG. ;;
  esac
done
if [[ ! $@ == '' ]] && ((params == 0 )); then
    echo "wrong arguments"
    exit 1
fi
  • more universal would be to increment params in the while loop, and afterwards check whether params is equal to the number of substrings in $@ of the form " -[aA-zZ]" – Łukasz Grabowski Dec 18 '16 at 00:52
  • I like this better than the "loop-in-a-loop" option, and it's fairly straight-forward. – Ogre Psalm33 Aug 18 '20 at 20:46