13

I defined the function f in Bash based on the example here (under "An option with an argument"):

f () {
  while getopts ":a:" opt; do
    case $opt in
      a)
        echo "-a was triggered, Parameter: $OPTARG" >&2
        ;;
      \?)
        echo "Invalid option: -$OPTARG" >&2
        return 1
        ;;
      :)
        echo "Option -$OPTARG requires an argument." >&2
        return 1
        ;;
    esac
  done
}

Whereas they use a script, I directly define the function in the shell.

When I first launch Bash and define the function, everything works: f -a 123 prints -a was triggered, Parameter: 123. But when I run the exact same line a second time, nothing is printed.

What's causing this behavior? It happens in Bash 3.2 and 4.3, but it works fine in Zsh 5.1. This is surprising because the example was supposed to be for Bash, not for Zsh.

shadowtalker
  • 1,328

4 Answers4

18

bash getopts use an environment variable OPTIND to keep track the last option argument processed. The fact that OPTIND was not automatically reset each time you called getopts in the same shell session, only when the shell was invoked. So from second time you called getopts with the same arguments in the same session, OPTIND wasn't changed, getopts thought it had done the job and do nothing.

You can reset OPTIND manually to make it work:

$ OPTIND=1
$ f -a 123
-a was triggered, Parameter: 123

or just put the function into a script and call the script multiple times.


zsh getopts is slightly different. OPTIND was normally reset to 1 each time upon exit from shell function.

cuonglm
  • 153,898
  • I'm pedantically accepting this answer because it's slightly more complete than the other one – shadowtalker Oct 03 '15 at 18:04
  • 1
    Added unset opt OPTARG OPTIND before every while getopts... call and it now works flawlessly. Thanks :) – Deep Oct 06 '18 at 09:41
  • I'm glad I found this, because now my bash scripts with white space seperated args and values don't work in zsh from the change. Do you have any pointers to maintain getopts portability between the two? – openCivilisation Jul 18 '20 at 08:15
  • It fixes my bash script, though I cannot fix it in zsh. In zsh, it starts as OPTIND=1 but the variable is OPTIND=2 after the script has run after sourcing. – t7e May 24 '22 at 19:43
8

It is a god habit to declare local variables in any function. If you declare $opt, $OPTARG and $OPTIND then getopts will work any times you call the function. Local variables are discarded after the function finished.

#!/bin/bash
function some_func {
  declare opt
  declare OPTARG
  declare OPTIND

  while getopts ":a:" opt; do
    echo $opt is $OPTARG
  done
}
  • 2
    Just declaring didn't work for me. I had to put unset opt OPTARG OPTIND before the while getopts... statement such that the value of each of these variables was unset. If I didn't do this and just did the declare, since in the given bash session where I had already used getopts once the OPTIND was present, it remained unchanged – Deep Oct 06 '18 at 09:40
  • 1
    nice solution, leaves the global OPTIND alone outside the function, though I prefer the local keyword to be more explicit, and more compact syntax like: local opt OPTARG OPTIND – SpinUp __ A Davis Jun 30 '22 at 04:21
2

You need to set OPTIND=1 at the start of function f. By default it is 1, but then gets incremented as your args are parsed. When you call getopts again it carries on where it left off. You can see this if your 2nd call is:

f -a 123 -a 999

when it will print the 999.

meuh
  • 51,383
  • Of course! Coming from R and Python I always get tripped up by scoping in the shell. Will it work if I declare local OPTIND inside the function before getopts is called? – shadowtalker Oct 03 '15 at 18:03
  • 1
    should do, good solution. – meuh Oct 03 '15 at 18:06
  • 2
    @shadowtalker local OPTIND will work on bash and busybox sh, but it can have problems on dash, see https://unix.stackexchange.com/questions/657991/dash-and-local-keyword-not-resetting-shell. Thus I started to use OPTIND=1 instead. – pevik Jul 12 '21 at 06:57
1

When getopt is called it keep track of processed options by the variable OPTIND.

Try the following:

#!/bin/bash

f () {
    printf "Intro OPTIND: %d\n" "$OPTIND"
    while getopts ":a:b:" opt; do
        printf "Current OPTIND: %d\n" "$OPTIND"
        case $opt in
            a)
                echo "-a was triggered, Parameter: $OPTARG" >&2
                ;;
            b)
                echo "-b was triggered, Parameter: $OPTARG" >&2
                ;;
        esac
    done
    printf "Exit OPTIND: %d\n" "$OPTIND"
}

echo "Run #1"
f "$@"
echo "Run #2"
f "$@"

Yield:

./test -a foo -b bar
Run #1
Intro OPTIND: 1
Current OPTIND: 3
-a was triggered, Parameter: foo
Current OPTIND: 5
-b was triggered, Parameter: bar
Exit OPTIND: 5
Run #2
Intro OPTIND: 5
Exit OPTIND: 5

As such you could do something like:

OPTIND=1

at start of the function. Or the, depending on situation, and usually better:

local OPTIND

If OPTIND was not used, as the function is implemented, the while loop would go forever. One can also use it to resume processing of arguments, after fail or what ever, call a different function if x or y and it will resume where previous left off etc.

Runium
  • 28,811