3

In the manpage for the version of getopt that comes with Mac OSX, an example is given that uses the construction args=$(getopt optstring $*); set -- $args. What does set -- $args do here?

Furthermore, consider the function and test string

f () {
    args=$(getopt o: $*)
    set -- $args
    for i; do
        echo $i
    done
}

f -o 123 xyz

In Bash 3.2 and 4.3, this produces

-o
123
--
xyz

but in Zsh 5.1, it produces

 -o 123 -- xyz

What causes the discrepancy? Is it because of Zsh's different word-splitting behavior, or something else? I tried different combinations of quoting and parameter expansion flags and couldn't really figure out what was going on under the hood in either shell.

2 Answers 2

2

This is bad code. getopt is pretty much unusable¹. Use the getopts shell builtin instead (it has two advantages: it can work, and it's available on all POSIX platforms).

What args=$(getopt optstring $*); set -- $args does is:

  • Split each command line argument at whitespace and replace it by the list of whitespace-delimited words.
  • Take each of the resulting word and interpret it as a wildcard pattern. If the pattern matches at least one file, replace the pattern by the list of matching files.
  • Pass the resulting list of words to the getopt command.
  • Store the output of the getopt command in the variable args. This is a string consisting of the command line arguments joined together with whitespace as the separator, and reordered to have options and their arguments, then --, then non-option operands.
  • Split this string at whitespace, interpret each word as a wildcard pattern, and replace the pattern by the list of matching files if there are any.
  • Use the resulting list as the positional arguments.

In Bourne/POSIX-style shells, $foo is the “split+glob” operator. You need double quotes to get the value of a variable: "$foo". But in zsh, $foo works differently: it expands to the value of foo except when foo is empty. So in zsh you get a single positional parameter.

The first use of the split+glob operator could be avoided by writing getopt optstring "$@": this would pass the positional parameters exactly to getopt. But when parsing the output of getopt, splitting is unavoidable (you could disable globbing though), because getopt uses spaces to separate arguments. The problem with getopt is that the output is ambiguous: there's no way to distinguish a space that was in an argument from a space that getopt added as a separator.

The right way to do this is with getopts. It's robust and portable.

while getopts optstring OPTLET; do
  # We got the option -$OPTLET
  case $OPTLET in
    …
  esac
done
shift $((OPTIND-1))
# Now the positional parameters are just the non-option operands

¹ At least the BSD version. The GNU version adds features that make it usable.

2
  • I never realized the shell would perform globbing on the result of unquoted parameter expansions. Just one tangential question about your getopts example: does it matter if I call shift in every case statement versus calling shift all at once at the end? Commented Oct 4, 2015 at 3:03
  • 1
    @ssdecontrol If you call shift inside the loop, getopts will get out of synch. You'd also have to update OPTIND, which is how getopts keeps track of where it's at. Commented Oct 4, 2015 at 13:08
1

set -- $args set the positional arguments base on content of $args. Now you will get the different behavior between zsh and other POSIX shells.

Because zsh does not perform Field Splitting by default, you will get one string, which is content of$args. You must to call splitting explicitly to get the same behavior as bash (and also other POSIX shells):

set -- ${=args}

bash perform Field Splitting on content of $args, produce four string as you got. You can check $# to know number of positional arguments after set -- $args.

Note that in case of bash, you should add set -f to turn off globbing as well.

You must log in to answer this question.

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