magicant / yash

Yet another shell
http://magicant.github.io/yash/
GNU General Public License v2.0
346 stars 30 forks source link

Yash 2.57 - OPTIND is set to "n:m" when getopts encounters unspecified option #86

Closed odkr closed 2 weeks ago

odkr commented 3 weeks ago

Description

In Yash 2.57, when an option is encountered by getopts that is not specified in the option string, then OPTIND is set to "n:m", where n is the index of the last option and m is some other number, the value of which does not follow a rule I could discern (in the MWE given below it happens to always be 2).

While POSIX.1 does not specify what OPTIND should be set to when an option is encountered by getopts that is not specified in the option string, the current behavior breaks the shift $((OPTIND - 1)) idiom and differs from mainstream shells (I've compared it to the ash, bash, dash, ksh, mksh, oksh, and zsh).

MWE

yash <<'EOF'
for args in '-X' '-a -X' '-a -b -X' '-a -b -c -X'
do
    while getopts abc opt $args 2>/dev/null
    do
        case $opt in
        ("?") break
        esac
    done
    printf 'args="%s" OPTIND=%s\n' "$args" "$OPTIND"
done
EOF

prints

args="-X" OPTIND=1:2
args="-a -X" OPTIND=2:2
args="-a -b -X" OPTIND=3:2
args="-a -b -c -X" OPTIND=4:2

Expected behaviour

dash <<'EOF'
for args in '-X' '-a -X' '-a -b -X' '-a -b -c -X'
do
    while getopts abc opt $args 2>/dev/null
    do
        case $opt in
        ("?") break
        esac
    done
    printf 'args="%s" OPTIND=%s\n' "$args" "$OPTIND"
done
EOF

prints

args="-X" OPTIND=2
args="-a -X" OPTIND=3
args="-a -b -X" OPTIND=4
args="-a -b -c -X" OPTIND=5

Environment

$ uname -a
Darwin a464-u027.phl.univie.ac.at 23.6.0 Darwin Kernel Version 23.6.0: Wed Jul 31 20:49:46 PDT 2024; root:xnu-10063.141.1.700.5~1/RELEASE_ARM64_T8103 arm64
$ yash --version
Yet another shell, version 2.57
Copyright (C) 2007-2024 magicant
This is free software licensed under GNU GPL version 2.
You can modify and redistribute it, but there is NO WARRANTY.
$ env | grep LANG
LANG=en_GB.UTF-8
magicant commented 2 weeks ago

The shift $((OPTIND - 1)) idiom is only valid after getopts has processed all options. You should not expect $OPTIND to have a portable value until getopts returns a non-zero exit status. Consider this script:

while getopts :ab opt -a -aXb -b x; do
  if [ "$opt" = "?" ]; then
    echo $OPTIND
  fi
done

Bash and ksh show 2 while busybox, dash, and mksh show 3. Zsh even seems to change its behavior depending on the emulation mode.

The current behavior of yash works as a reminder that the $OPTIND value shouldn't be relied upon, so I would decline to change the behavior.

P.S. You don't necessarily have to break when you encounter an unknown option. You can keep calling getopts to let it process the remaining options.

odkr commented 2 weeks ago

Thanks for the quick reply and the example! And while I'm thanking you, also thank you for Yash. I had tested my MWE with bash, dash, and mksh, and in that case they just happen to agree. And since the MWE is derived from a script that is also tested with other shells, I wrongly inferred that the behavior is uniform. My bad.