60

I'd like to use getopts inside a function that I have defined in my .bash_profile. The idea is I'd like to pass in some flags to this function to alter its behavior.

Here's the code:

function t() {
    echo $*
    getopts "a:" OPTION
    echo $OPTION
    echo $OPTARG
}

When I invoke it like this:

t -a bc

I get this output:

-a bc
?
 

What's wrong? I'd like to get the value bc without manually shifting and parsing. How do I use getopts correctly inside a function?

EDIT: corrected my code snippet to try $OPTARG, to no avail

EDIT #2: OK turns out the code is fine, my shell was somehow messed up. Opening a new window solved it. The arg value was indeed in $OPTARG.

Adrian Frühwirth
  • 42,970
  • 10
  • 60
  • 71
Magnus
  • 10,736
  • 5
  • 44
  • 57

3 Answers3

126

As @Ansgar points out, the argument to your option is stored in ${OPTARG}, but this is not the only thing to watch out for when using getopts inside a function. You also need to make sure that ${OPTIND} is local to the function by either unsetting it or declaring it local, otherwise you will encounter unexpected behaviour when invoking the function multiple times.

t.sh:

#!/bin/bash

foo()
{
    foo_usage() { echo "foo: [-a <arg>]" 1>&2; exit; }

    local OPTIND o a
    while getopts ":a:" o; do
        case "${o}" in
            a)
                a="${OPTARG}"
                ;;
            *)
                foo_usage
                ;;
        esac
    done
    shift $((OPTIND-1))

    echo "a: [${a}], non-option arguments: $*"
}

foo
foo -a bc bar quux
foo -x

Example run:

$ ./t.sh
a: [], non-option arguments:
a: [bc], non-option arguments: bar quux
foo: [-a <arg>]

If you comment out # local OPTIND, this is what you get instead:

$ ./t.sh
a: [], non-option arguments:
a: [bc], non-option arguments: bar quux
a: [bc], non-option arguments:

Other than that, its usage is the same as when used outside of a function.

Adrian Frühwirth
  • 42,970
  • 10
  • 60
  • 71
  • 1
    1.) The `1` in `1>&2` is not necessary. 2.) You missed to define `a`, `o` and `OPTARG` as local. 3.) The `exit` will not exit the script but just a sub-shell. In order to exit the script it is necessary to `set -e` in the outer shell and `exit 1` in the sub-shell. The example does not trigger the problem, but a `MSG=$(foo ...)` will do it. – ceving Oct 12 '14 at 18:25
  • 2
    @ceving 1) This is a matter of coding style but, no, it's not necessary as per the language definition. 2) Agreed, those should be local. 3) As you said, the exit *will* exit the script in my example. Of course `exit` won't exit sub shells but this is not a specific problem to this question. You don't have to use `set -e`, you just have to make sure to catch the error and `MSG=$(foo ...) || die` works just as well. `set -e` is one solution to the problem [but it's not idiot-proof](http://mywiki.wooledge.org/BashFAQ/105) and I, as many others, don't recommend using it. – Adrian Frühwirth Oct 13 '14 at 06:59
  • 1
    Just pointing out that even if you're not using OPTIND in the function (e.g., with `shift;`), it still needs to be localised in order to have predictable behaviour. –  Aug 23 '16 at 12:58
  • @ceving, you can look here, https://stackoverflow.com/questions/61566331/why-is-optind-messing-my-positional-params - I added `local` but did not helped – Herdsman May 02 '20 at 22:21
20

Here is simple example of getopts usage within shell function:

#!/usr/bin/env bash
t() {
  local OPTIND
  getopts "a:" OPTION
  echo Input: $*, OPTION: $OPTION, OPTARG: $OPTARG
}
t "$@"
t -a foo

Output:

$ ./test.sh -a bc
Input: -a bc, OPTION: a, OPTARG: bc
Input: -a foo, OPTION: a, OPTARG: foo

As @Adrian pointed out, local OPTIND (or OPTIND=1) needs to be set as shell does not reset OPTIND automatically between multiple calls to getopts (man bash).

The base-syntax for getopts is:

getopts OPTSTRING VARNAME [ARGS...]

and by default, not specifying arguments is equivalent to explicitly calling it with "$@" which is: getopts "a:" opts "$@".

In case of problems, these are the used variables for getopts to check:

  • OPTIND - the index to the next argument to be processed,
  • OPTARG - variable is set to any argument for an option found by getopts,
  • OPTERR (not POSIX) - set to 0 or 1 to indicate if Bash should display error messages generated by the getopts.

Further more, see: Small getopts tutorial at The Bash Hackers Wiki

Community
  • 1
  • 1
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • Very thoughtfully laid out answer. I don't know if it works, but great job. – Drew Dec 29 '15 at 23:34
  • @kenorb, you can look here -> https://stackoverflow.com/questions/61566331/why-is-optind-messing-my-positional-params, added `local` but did not helped – Herdsman May 02 '20 at 22:22
6

The argument is stored in the varable $OPTARG.

function t() {
  echo $*
  getopts "a:" OPTION
  echo $OPTION
  echo $OPTARG
}

Output:

$ t -a bc
-a bc
a
bc
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
  • Sorry I incorrectly pasted my code snippet... I also echo $OPTARG, that's the 3rd line which is blank. Any other ideas? – Magnus May 20 '13 at 20:08
  • @Magnus Probably because you called the function several times and `$OPTIND` isn't defined locally (see Adrian's answer). While I'm grateful that you accepted my answer, you should probably rather accept his instead. – Ansgar Wiechers May 20 '13 at 20:23
  • The code works only when pasting into shell, but not in the script. – kenorb Dec 29 '15 at 21:33
  • 1
    @kenorb It works just fine from a script. I'm not making this up. – Ansgar Wiechers Dec 29 '15 at 22:17
  • Ok, I wasn't sure how `t` is called, I had to call as `t -a bc` inside the script or add `t "$@"`, initially I thought it's going to work without. – kenorb Dec 29 '15 at 22:22