bash getopts pitfalls in CI
getopts parses single-dash short options from a fixed optstring. Common CI failures come from not resetting OPTIND when parsing twice, forgetting the leading : for silent error handling, expecting long --options (getopts does not support them), or shifting the wrong count so positional args are lost.
What this error means
Flags are ignored, "illegal option" is printed for valid flags on a second parse, or positional arguments after the options are missing. Long options like --verbose are treated as a single - cluster.
while getopts "ab:" opt; do # no leading colon: getopts prints its own errors
case "$opt" in
a) all=1 ;;
b) name="$OPTARG" ;;
esac
done
# parsing again later without: OPTIND=1 -> options are skippedCommon causes
OPTIND not reset between parses
getopts uses the global OPTIND to track position. Calling a parse loop a second time without resetting OPTIND=1 continues from the old index and skips options.
Missing leading colon and long-option assumptions
Without a leading : in the optstring, getopts prints its own error messages instead of letting you handle ? and :. And getopts only handles short options; --long forms are not parsed.
How to fix it
Use a leading colon and handle : and ?
Put : first in the optstring for silent errors, then handle a missing argument (:) and an unknown option (?) yourself.
while getopts ":ab:" opt; do
case "$opt" in
a) all=1 ;;
b) name="$OPTARG" ;;
:) echo "-$OPTARG needs a value" >&2; exit 1 ;;
\?) echo "unknown option -$OPTARG" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))Reset OPTIND before reparsing
When a function or loop parses options more than once, set OPTIND=1 first so parsing starts fresh.
OPTIND=1
while getopts ":v" opt; do ...; doneHow to prevent it
- Always
shift $((OPTIND - 1))after the loop to reach positionals. - Reset
OPTIND=1before each independent parse. - Use a helper like
getopt(GNU) if you need long options.