5

I'm trying to understand why getopts seems to ignore all arguments if an "unnnamed" argument precedes any named arguments.

Using an example from http://wiki.bash-hackers.org/howto/getopts_tutorial,

#!/bin/bash

while getopts ":a" opt; do
  case $opt in
    a)
      echo "-a was triggered!" >&2
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      ;;
  esac
done

And observing the outcome:

$ ./opt_test
$ ./opt_test -a
-a was triggered!
$ ./opt_test -a -f
-a was triggered!
Invalid option: -f
$ ./opt_test a -a -f
$ ./opt_test a -a
$ ./opt_test a -f
$ ./opt_test lala -f
$ 

So prepending an unnamed argument (an argument without a dash) seems to make getopts ignore all arguments.

Why is this and how can I work around it? I'd like my program to be able to catch such things and print a usage screen.

miken32
  • 42,008
  • 16
  • 111
  • 154
gamen
  • 987
  • 6
  • 12

4 Answers4

3

A workaround I use is to pass an array slice to getopts:

Instead of this:

while getopts ":a" opt; do

Try this:

while getopts ":a" opt ${@:2}; do

This passes getopts the second through last arguments to parse and works correctly to ignore the first argument.

Dave Stern
  • 511
  • 2
  • 7
  • 13
3

It is fairly standard behavior for programs to stop processing options when they encounter the first non-option argument. This is very often exactly what you want. For example, think about this:

ssh someremotehost ls -l

If ssh tried to process options after the first non-option argument, you would never be able to pass parameters to remote commands. Another standard supported by getopt is that options processing stops explicitly at a -- argument, so you can do things like this to remove a file named -f:

rm -- -f

If you really want to process options anywhere on the command line, you can write your own options processing routine. It's not really all that difficult, and you can implement support for long options (--this-is-a-long-option) as well.

larsks
  • 277,717
  • 41
  • 399
  • 399
  • What you say is I suppose true. However whereas this might be a common result from a user perspective, this is hardly standard behavior from a programming standpoint; getopt(3) and getopt_long(3) behave very differently, as does Perls "GetOptions". Anyhow, now I know. Thanks for taking the time to reply! – gamen Nov 18 '11 at 15:53
  • 1
    This is in fact the POSIX behavior. You'll note that getopt(), in the presence of the POSIXLY_CORRECT environment variable, will behave the same way. – larsks Nov 18 '11 at 18:23
2

Quoting from getopts documentation:

"Any of the following shall identify the end of options: the special option "--" , finding an argument that does not begin with a '-' , or encountering an error."

Explanatory Notes:

  1. getopts() supports the Utility Syntax Guidelines opengroup.org, 2023. Which defines command-line syntax as UTILITY_NAME OPTION+ARGUMENTS OPERANDS, they call unnamed arguments OPERANDS
  2. The Open Group exists to simplify integration with other peoples code. Following their Utility Syntax Guidelines will reduce the complexity of your code maintenance. These are relevant to your question:
  1. multiple arguments can be stated for an option,seperate arguments with a comma
  2. options should precede operands (unnamed) on the command line.
  3. the -- indicates no more options all succeeding arguments are operands.
dank8
  • 361
  • 4
  • 20
Adam Zalcman
  • 26,643
  • 4
  • 71
  • 92
0

To let the options come before unnamed (positional) arguments, use shift afterwords to clean up,

e.g. to have an option without a value:

while getopts "u" opt; do
case $opt in
  u) USE_USERNAMES="TRUE"
  ;;
  \?) echo "Invalid option -$OPTARG" >&2
  ;;
esac
done

# Clean-up after arguments parsing
if test "$USE_USERNAMES"; then
  shift 
fi

Or, with a value:

while getopts "s:" OPT; do
case $OPT in
  s) SUBMISSIONS="$OPTARG"
  ;;
  \?) echo "Invalid option -$OPTARG" >&2
      exit 1
  ;;
esac
done

# Clean-up arguments and assign default values AFTER arguments parsing
if test "$SUBMISSIONS"; then
  shift 2
else
  SUBMISSIONS="./logins.txt"
fi

# Move on to unnamed (positional) arguments
if ! test "$1"; then
  echo "ERROR: Required first argument missing"
  exit 1
fi

# Use arguments
echo "SUBMISSIONS: $SUBMISSIONS"
echo "FIRST ARG: $1"

Reference:

Community
  • 1
  • 1
Josiah Yoder
  • 3,321
  • 4
  • 40
  • 58