Last update: 23-Oct-2022
Author: R. Koucha
Hidden feature of getopts command
Introduction

getopts is a well known utility to parse the command line of the shell scripts. Its synopsis is: getopts optstring name [arg...]

It appears that the option string can be configured to make the "-" character be considered as an option.

Recap on the option string

The option string lists the letters accepted as options by the shell scripts. If the letter is followed by a colon, a parameter is expected right after the option. For example a command accepting "-h" and "-l" followed by a "name" parameter will be specified as:

getopts "hl:" optname

Each time it is invoked, getopts shall place the value of the next option in the shell variable specified by the name operand (i.e. "optname" in the above example), the value of the following parameter if expected in OPTARG and the index of the next argument to be processed in the shell variable OPTIND.
Let's considered the following example program to illustrate this:

#!/bin/bash

hopt=
lopt=

while getopts 'hl:' optname
do
  case $optname in
    h) hopt=1;;
    l) lopt=1
       lval="$OPTARG";;
    ?) echo "Usage: $0: [-h] [-l name]" >&2
       exit 1;;
  esac
done

[ ! -z "$hopt" ] && echo "Option -h specified"

[ ! -z "$lopt" ] && echo "Option -l specified with optname '$lval'"

shift $(($OPTIND - 1))
echo "Remaining arguments are: $*"

Example of executions:

$ ./t.sh   
Remaining arguments are: 
$ ./t.sh -g
./t.sh: illegal option -- g
Usage: ./t.sh: [-h] [-l name]
 ./t.sh -h
Option -h specified
Remaining arguments are: 
$ ./t.sh -h -l
./t.sh: option requires an argument -- l
Usage: ./t.sh: [-h] [-l name]
$ ./t.sh -h -l foo
Option -h specified
Option -l specified with optname 'foo'
Remaining arguments are: 
 ./t.sh -h -l foo p1 p2
Option -h specified
Option -l specified with optname 'foo'
Remaining arguments are: p1 p2
Using "-" as an option

Sometimes, we see the minus character followed by a colon in the option string. Let's add it in our above example script:

#!/bin/bash

hopt=
lopt=
mopt=

while getopts 'hl:-:' optname
do
  case $optname in
    h) hopt=1;;
    l) lopt=1
       lval="$OPTARG";;
    -) mopt=1
       mval="$OPTARG";;
    ?) echo "Usage: $0: [-h] [-l name]" >&2
       exit 1;;
  esac
done

[ ! -z "$hopt" ] && echo "Option -h specified"

[ ! -z "$lopt" ] && echo "Option -l specified with optname '$lval'"

[ ! -z "$mopt" ] && echo "Option -- specified with optname '$mval'"

shift $(($OPTIND - 1))
echo "Remaining arguments are: $*"

Let's play with it:

$ ./t1.sh -h -l foo -- j 
Option -h specified
Option -l specified with optname 'foo'
Remaining arguments are: j
$ ./t1.sh -h -l foo --j 
Option -h specified
Option -l specified with optname 'foo'
Option -- specified with optname 'j'
Remaining arguments are: 
$ ./t1.sh -h -lfoo --joke 
Option -h specified
Option -l specified with optname 'foo'
Option -- specified with optname 'joke'
Remaining arguments are: 

The above tries show that passing "--something" to the script (without spaces after "--") makes it accept "--" as an option and report "something" in OPTARG. Would it be a "hidden" feature to manage gnu-like long options but without parameters (i.e. only the "--long_opt_name") or am I promoting the side effect of a bug?
Nevertheless, if spaces are put after the double "-", the latter continues to play its usual documented role separating options from additional parameters as we could see in the first try in the above display.

Verification in the source code

As getopts is a builtin of bash, I downloaded its source code (version 5.0) from here. The builtins are located in the eponym sub-directory. getopts source code is builtins/getopts.def. For each argument on the command line, it calls sh_getopt(argc, argv, optstr). This function is defined in builtins/getopt.c:

[...]
int
sh_getopt (argc, argv, optstring)
     int argc;
     char *const *argv;
     const char *optstring;
{
[...]
  /* Look at and handle the next option-character.  */

  c = *nextchar++; sh_charindex++;
  temp = strchr (optstring, c);

  sh_optopt = c;

  /* Increment `sh_optind' when we start to process its last character.  */
  if (nextchar == 0 || *nextchar == '\0')
    {
      sh_optind++;
      nextchar = (char *)NULL;
    }

  if (sh_badopt = (temp == NULL || c == ':'))
    {
      if (sh_opterr)
    BADOPT (c);

      return '?';
    }

  if (temp[1] == ':')
    {
      if (nextchar && *nextchar)
    {
      /* This is an option that requires an argument.  */
      sh_optarg = nextchar;
      /* If we end this ARGV-element by taking the rest as an arg,
         we must advance to the next element now.  */
      sh_optind++;
    }
      else if (sh_optind == argc)
    {
      if (sh_opterr)
        NEEDARG (c);

      sh_optopt = c;
      sh_optarg = "";   /* Needed by getopts. */
      c = (optstring[0] == ':') ? ':' : '?';
    }
      else
    /* We already incremented `sh_optind' once;
       increment it again when taking next ARGV-elt as argument.  */
    sh_optarg = argv[sh_optind++];
      nextchar = (char *)NULL;
    }
  return c;
}

In the previous source lines, nextchar points on the option character (i.e. the one located right after '-') in argv[] and temp points on the option character in the optstring (which is '-'). We can see when temp[1] == ':' (i.e. the optstring specifies "-:"), sh_optarg is set with the incremented value of nextchar which is the first letter of the option name located behind "--".
In our example, where optstring is "hl:-:" and we pass to the script "--joke", the above code does:

optstring = "hl:-:"
                ^
                |
               temp

argv[x] = "--joke"
            ^^
           /  \
          c  nextchar (+ 1)

temp[1] == ':' ==> sh_optarg=nextchar="joke"
Conclusion

Hence, with the above algorithm in bash, when ":-" is specified in the option string, any "--name" option on the command line reports "name" in OPTARG variable.
This is merely the output of the code path when the parameter is concatenated to the option name (e.g. -xfoo = option "x" and parameter "foo", --foo = option "-" and parameter "foo").
Is such an undocumented behavior advised? This behaviour may change after some future fixes or evolution in the source code of the command.

About the author

The author is an engineer in computer sciences located in France. He can be contacted here.