3

I know how to handle optional arguments using getopts in Bash

#!/bin/bash

while getopts ":a:p:drh" opt; do
  case "$opt" in
    a) echo $OPTARG;;
  esac
done

But if my script expects ./script.sh arg1 [options], how do I tell getopts to skip arg1?

$ ./script.sh -a 1234
1234
$ ./script.sh arg1 -a 1234
$ ./script.sh arg1
$ ./script.sh -a 1234 arg1
1234

As I see I can handle the argument if the argument is put in the last position. What "regex" do I need in the getopts to allow my positional argument to be in front of the optional argument?

The suggestions from How to allow non-option arguments in any order to getopt? seems to be reordering the arguments.

Community
  • 1
  • 1
CppLearner
  • 16,273
  • 32
  • 108
  • 163

2 Answers2

5

Here's how I'd do it. You can have many several non-optional arguments placed anywhere.

#!/bin/bash

while [ $# -gt 0]; do
  while getopts ":a:p:drh" opt; do
    case "$opt" in
      a) echo $OPTARG; shift;;
      p) echo $OPTARG; shift;;
      d) echo Option d;;
      r) echo Option r;;
      h) echo Option h;;
      \?) echo unknown Option;;
      :) echo missing required parameter for Option $OPT;;
    esac
    shift
    OPTIND=1
  done
  if [ $# -gt 0 ]; then
    POSITIONALPARAM=(${POSITIONALPARAM[@]} $1)
    shift
    OPTIND=1
  fi
done

echo ${POSITIONALPARAM[@]}

The inner loop parses the parameters. Whenever a non-option parameter is encountered, it will exit the inner loop. The external loop will grab the next non-option parameter. The inner loop will then pickup after the non-option parameter is removed then resumes reading the next optional parameters and so on.

As Chrono Kitsune suggested that I explain, the shift removes the first argument (optional or not) and moves everything one position to the left. $1 is removed, $2 becomes $1, $3 becomes $2 so on and so forth. This gives the script the control to move the non-optional argument until it becomes $1.

Both shift and resetting OPTIND make this possible. Thanks to Chrono Kitsune for the suggestion that OPTIND should be reset to 1 instead of 0.

./sample.bash -a one -p two more -d sample

Output:

one
two
Option d
more sample

Now let's call the script with arguments in different positions.

./sample.bash more -a one -p two -d sample

Output:

one
two
Option d
more sample
Community
  • 1
  • 1
alvits
  • 6,550
  • 1
  • 28
  • 28
  • 1
    `OPTIND` should be 1, not 0. Probably also a good idea to note where the `shift`'s are since they are crucial to this method. –  Jan 07 '14 at 00:33
  • OPTIND can be 0 or 1 as long as it isn't greater than 1. The only requirement is to reset OPTIND. However, if you do know that there are implementation where setting OPTIND to 0 does not work, then I'd have to start resetting to 1 instead of 0 as I've been used to. And thank you for taking the time to let me know. Knowledge is power. – alvits Jan 07 '14 at 00:44
  • 1
    "If the application sets OPTIND to the value 1, a new set of parameters can be used... Any other attempt to invoke getopts ... with an OPTIND value modified to be a value other than 1, produces unspecified results." [(Source)](http://pubs.opengroup.org/onlinepubs/7999959899/utilities/getopts.html) –  Jan 07 '14 at 00:51
  • 1
    @Chrono Kitsune - I updated it to reset `OPTIND` to 1 instead of 0. – alvits Jan 07 '14 at 00:56
  • Cool, @alvits. I gave him the credit since he answered first and it was actually simpler to understand, but I did learn from your answer. Thanks. Upvoted! – CppLearner Jan 10 '14 at 01:45
1

If you know the number of positional arguments then you can do something like this:

#!/bin/bash

VAR=$1
shift
echo $VAR

while getopts ":a:p:drh" opt; do
  case "$opt" in
    a) echo $OPTARG;;
  esac
done

Example:

> ./test.sh mist -a 2345 -p 45
mist
2345
Christian Fritz
  • 20,641
  • 3
  • 42
  • 71