Skip to content
Shell & TerminalDeep Dive Published Updated 4 min readViews unavailable

Portable Shell Option Parsing with getopts: State, Errors, and Reentrancy

A POSIX getopts pattern covering OPTIND, OPTARG, leading-colon diagnostics, required operands, repeated calls, function scope, and portable limitations.

POSIX getopts parses short options from positional parameters while maintaining progress in OPTIND and returning option arguments in OPTARG. It avoids external getopt portability differences and unsafe re-evaluation. It does not provide portable long options, optional option arguments, or automatic semantic validation.

Define a small grammar

In the option string, a letter followed by : requires an argument. A leading : selects silent error reporting so the script controls diagnostics. For a command supporting -n name, repeatable -v, and -h:

usage() {
    printf 'Usage: %s [-v] -n name file...\n' "${0##*/}" >&2
}

name=
verbose=0
while getopts ':n:vh' opt; do
    case $opt in
        n) name=$OPTARG ;;
        v) verbose=$((verbose + 1)) ;;
        h) usage; exit 0 ;;
        :) printf 'Option -%s requires an argument\n' "$OPTARG" >&2
           usage; exit 2 ;;
        \?) printf 'Unknown option: -%s\n' "$OPTARG" >&2
            usage; exit 2 ;;
    esac
done
shift "$((OPTIND - 1))"

[ -n "$name" ] || { printf 'Missing -n name\n' >&2; usage; exit 2; }
[ "$#" -gt 0 ] || { printf 'Missing file operand\n' >&2; usage; exit 2; }

Quote $OPTARG, the shift count, and every later operand. Parsing establishes syntax only; validate that names, numbers, paths, and mutually exclusive modes are semantically valid afterward.

Error mode changes return values

Without a leading colon, an unknown option yields ?, a missing required argument also yields ?, and the shell may print a diagnostic. With the leading colon, unknown options yield ? and missing arguments yield :, with the relevant option character placed in OPTARG. The silent form prevents duplicate or implementation-specific messages.

-- conventionally ends option parsing. getopts stops at it and the following shift removes it with parsed options. A lone operand beginning with - therefore needs -- before it. Document that behavior in usage and pass -- when invoking other utilities with user-controlled operands.

POSIX does not require long options such as --verbose. Do not fake them by rewriting arguments with eval; values can contain spaces, glob characters, command substitutions, or quotes. If long options are essential, choose a target-specific parser or implement a direct case "$1" loop that never reconstructs shell source.

OPTIND is state

OPTIND starts at 1 for a new shell and advances as options are parsed. Calling a parser twice in one shell requires resetting it according to the shell’s supported behavior. POSIX specifies setting OPTIND=1 for a new set of parameters in the defined use cases, but changing both the parameter set and parsing context in complex nested calls can have unspecified interactions.

Functions share shell variables unless the shell provides and the script uses non-POSIX local scope. A library parser can accidentally consume or overwrite the caller’s OPTIND and OPTARG. For strict portability, parse the program’s top-level options once. If a function parses its own explicit arguments, save caller state where the target shells support the intended semantics and test every shell in the compatibility matrix.

Do not rely on implementation-specific permutation of options after operands. Portable callers put options before operands, and the script documents that grammar. If a subcommand has its own options, stop the top-level parser at the subcommand, shift it away, reset parsing state according to the declared shell, and call a separate parser with the remaining positional parameters. This keeps tool -v build -j 4 from accidentally interpreting -j as a global option.

Localization also belongs at the diagnostic layer, not in the option grammar. Option letters, exit codes, and machine-readable output should remain stable while human usage/error strings may be translated.

Test the grammar as an interface

Cover combined flags (-vv), attached arguments (-nvalue), separate arguments, empty strings, missing values, unknown options, --, operands beginning with a dash, repeated flags, and extra operands. Assert exit status 0 for help/success and a documented nonzero status for usage errors.

getopts is reliable because it is intentionally narrow. A clear option string, controlled diagnostics, quoted state, post-parse validation, and explicit portability limits produce a CLI users and automation can depend on.

Related:

Sources:

Comments