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.

4 Answers 4

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.

4
  • I'm pedantically accepting this answer because it's slightly more complete than the other one Oct 3, 2015 at 18:04
  • 1
    Added unset opt OPTARG OPTIND before every while getopts... call and it now works flawlessly. Thanks :)
    – Deep
    Oct 6, 2018 at 9: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? Jul 18, 2020 at 8: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, 2022 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
  • 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 6, 2018 at 9: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 Jun 30, 2022 at 4: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.

3
  • 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? Oct 3, 2015 at 18:03
  • 1
    should do, good solution.
    – meuh
    Oct 3, 2015 at 18:06
  • 2
    @shadowtalker local OPTIND will work on bash and busybox sh, but it can have problems on dash, see unix.stackexchange.com/questions/657991/…. Thus I started to use OPTIND=1 instead.
    – pevik
    Jul 12, 2021 at 6: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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .