Skip to content
Shell & TerminalHow-To Published Updated 8 min readViews unavailable

Zsh zparseopts: Reliable Option Parsing in Shell Functions

Parse Zsh command options with zparseopts while preserving operands, validating unknown flags, and handling required arguments without fragile shifts.

Zsh’s zparseopts builtin parses recognized options from a function’s positional parameters and can remove them while preserving the remaining operands. It is useful for Zsh scripts and shell functions that need short options, long options, required arguments, or explicit unknown-option handling. Unlike POSIX getopts, zparseopts is a Zsh-specific facility with array-oriented output and its own rules for option terminators and interspersed arguments.

The parser does not make a command-line interface correct by itself. The function still has to validate option combinations, distinguish options from operands, apply defaults, and define how repeated flags behave. Read the remaining $@ after parsing instead of assuming every non-option was consumed, and never evaluate a user-supplied argument as shell code.

Load the module and declare the output array

zparseopts is provided by the zsh/zutil module. Load it in the scope where the parser is used, or arrange for it to be autoloaded by the application’s initialization. A completion function or script should not assume that an unrelated plugin happened to load the module first.

parse_options() {
	emulate -L zsh
	zmodload zsh/zutil || return 2
	local -a options

	zparseopts -D -F -a options v -output: || return 2
	print -rl -- 'recognized option fields:' "${options[@]}"
	print -rl -- 'remaining operands:' "$@"
}

The specs recognize -v and --output ARG. -a options collects recognized option words and their arguments in one normal array. The colon after -output marks its argument as mandatory; the option and its argument are stored as separate array elements. -D removes recognized options from the function’s positional parameters, while -F makes an unknown option-like parameter an error. emulate -L zsh localizes shell emulation options to the function instead of changing the caller’s interactive environment.

This is an illustrative parser, not a complete application. Add validation for empty or repeated output paths, check the file operation that consumes the path, and print usage on an error. zparseopts reports the parsing failure through its status; the function should return a nonzero status and avoid continuing with a partial configuration.

Define option names and argument requirements

Each specification describes an option name without the first leading hyphen. A short option v represents -v; a name beginning with a hyphen, such as -output, represents the long option --output. A colon means the option takes an argument. One colon requires an argument; two colons make it optional. The argument may appear in the same positional parameter or in the next one, subject to the parser’s documented rules.

For a required option argument, decide whether the option and its value should be separate array elements or combined in one. The ordinary name: form stores a mandatory argument separately; name:- stores it together with the option name. For optional arguments, Zsh stores the optional value with the option name, which makes an explicitly empty argument indistinguishable from some other forms. Choose the representation based on the consumer rather than relying on positional guesses.

Long option names use one fewer leading hyphen in the spec than in the command line. For example, -config: describes --config VALUE, and -config:- describes the form where the option and argument are kept in one element. An option token like --config=production is not split at = as many GNU-style parsers do: with -config: it is read as --config with an argument whose value begins with =production. Document the accepted spelling and test it explicitly.

The parser can also represent aliases and repeated values through separate specs or destination arrays. By default, repeated occurrences of a no-argument option do not necessarily accumulate as the caller expects; use the + form when the specification should append every occurrence. Keep duplicate and conflict policy in application code: decide whether the last value wins, the first wins, repeated values are collected, or duplicates are rejected.

Control where parsing stops

By default, parsing stops at the first parameter that is not described by a spec. That is convenient for a command grammar where options must precede operands. The parser also stops at a standalone - or -- marker. This makes it possible for the application to pass filenames that begin with a hyphen as operands after an explicit terminator.

-E asks the parser to continue looking for recognized options after ordinary positional parameters. Use it only if the command intentionally supports interspersed options. -F provides basic validation: when an unrecognized option-like parameter is encountered, parsing stops, reports an error, and does not perform removal or update the output arrays. This is useful when an unknown flag should fail rather than silently become a filename.

zparseopts -E -D -F -a options v -output:

Here -E permits recognized options after operands, -D removes recognized option tokens, and -F rejects an option-like token with no matching specification. The standalone -- remains a parsing boundary. Without -E, the first operand ends normal option parsing, so later option-looking words remain in $@ and are the caller’s responsibility.

Avoid enabling interspersed parsing just to make a test pass. It changes whether a positional filename that resembles an option is treated as an option. If the command accepts arbitrary filenames, document -- and test both a filename beginning with - and an unknown flag before and after the terminator.

Preserve caller arguments and parser state

-D mutates the function’s positional parameters by removing recognized options. This is often the desired behavior because the remaining $@ can be passed to another function or command. Parse inside a function when possible so the mutation is scoped to that invocation. If parsing directly at top level, remember that the shell’s positional parameters are the script’s live argument vector.

Declare output arrays before parsing and initialize them deliberately. The -K flag can preserve an array’s existing values when none of the corresponding options is used; without it, an array associated with a matched option can be replaced according to the parser’s rules. This matters when defaults have already been placed in the array. Use local arrays for per-invocation state so one call does not leak options into the next.

Do not use eval to dispatch parsed options. The array holds data; compare recognized option names and values explicitly, validate a path or enum, and then call the intended operation. A parser that accepts --output does not establish that the user may write to every path, and shell quoting does not replace application authorization.

For a parser shared by several scripts, document its accepted spec forms and return statuses. Keep output arrays in a stable shape and avoid changing the mapping from options to elements without updating callers. A helper that returns a status and named data is easier to test than one that mutates globals and prints an undocumented positional protocol.

Handle unknowns, missing values, and conflicts

Test an unknown option, a missing required argument, an option with an empty value, a repeated option, a short option followed by text, an operand beginning with a hyphen, and the -- terminator. A missing required argument is an error regardless of whether -F is used. Verify that a failed parse does not cause the script to continue with partially applied options.

Overlapping option names can be ambiguous when one option takes an argument. For no-argument options, the longest matching name wins. If an argument-taking option overlaps another name, the last matching spec can win. Design option names to avoid prefixes that can be confused, and order specs deliberately where the grammar requires it. It is safer to reject an ambiguous interface than to depend on a fragile order nobody remembers.

-K, -M, and -E alter assignment or scanning behavior. Introduce them only when the command needs those semantics and add a focused test for the exact behavior. In particular, the mapping mode -M can direct different option spellings into shared destinations, but mixing append forms and mapping rules can make results difficult to interpret.

Test in Zsh, not in an unrelated shell

zparseopts is unavailable in Bash, dash, and POSIX sh. A script that uses it should declare a Zsh interpreter and be run with Zsh in tests and automation. Syntax checks with zsh -n can catch malformed shell syntax but do not prove that option specs behave as intended. Execute tests with representative positional parameters and inspect both the output arrays and the remaining $@.

Keep the test matrix small and explicit. Check each supported option in its separate-argument and combined-argument form if both are intended, repeated occurrences, ordering, terminators, unknowns, and exact status on failure. Test under the target Zsh version because module availability and completion or parser behavior can evolve. Avoid relying on a developer’s plugin configuration to provide zsh/zutil.

Use zparseopts when an explicitly Zsh-only function benefits from recognized-option extraction and array-based results. Load zsh/zutil, define each spec precisely, choose stop and error behavior intentionally, preserve operands, and validate the resulting data before it reaches filesystem or process operations.

Related:

Sources:

Comments