util-linux getopt in Bash: Long Options, Quoted Arguments, and the eval Boundary
Parse long options with util-linux getopt, preserve argv boundaries, detect incompatible implementations, and constrain eval to parser-generated syntax.
getopts and getopt are different interfaces. POSIX getopts is a shell utility that updates shell variables while it parses short options. The util-linux getopt(1) program is an external parser with long-option support and a quoting mode that can serialize arguments for a shell to reconstruct. That second interface can preserve spaces and shell metacharacters, but its normal pattern includes eval. Treat that as a narrowly controlled parser boundary, not a general-purpose way to execute a string.
This guide targets Bash scripts on systems with the enhanced util-linux implementation. The command name alone does not prove those semantics: older implementations may accept getopt but cannot preserve arbitrary argument boundaries. If the script must run on macOS, BSD, minimal containers, or an unknown host, prefer POSIX getopts or a parser written in the script’s implementation language instead.
Pin the option grammar and stop at the first operand
Declare each accepted option and whether it requires a value. This example accepts -c/--config and -o/--output with required values, plus -v/--verbose and -h/--help as flags:
#!/usr/bin/env bash
usage() {
printf 'Usage: %s [-v|--verbose] [-c|--config FILE] [-o|--output FILE] [-h|--help] [--] INPUT\n' "${0##*/}" >&2
}
# This variable disables util-linux getopt's quoted-output mode when set.
unset GETOPT_COMPATIBLE
# util-linux getopt -T uses status 4. Old implementations return success
# instead, so do not interpret their output as shell syntax.
if command getopt --test >/dev/null 2>&1; then
printf '%s\n' 'this script requires enhanced util-linux getopt' >&2
exit 2
else
test_status=$?
if (( test_status != 4 )); then
printf 'cannot verify util-linux getopt (status %d)\n' "$test_status" >&2
exit 2
fi
fi
if parsed=$(command getopt \
--shell bash \
--options '+hc:o:v' \
--longoptions 'help,config:,output:,verbose' \
--name "${0##*/}" -- "$@"); then
:
else
parser_status=$?
usage
exit "$parser_status"
fi
# This is the sole eval boundary: parsed must be unmodified output from the
# verified util-linux executable and must use Bash quoting conventions.
eval "set -- $parsed"
config=''
output=''
verbose=0
while :; do
case $1 in
-h|--help) usage; exit 0 ;;
-c|--config) config=$2; shift 2 ;;
-o|--output) output=$2; shift 2 ;;
-v|--verbose) verbose=1; shift ;;
--) shift; break ;;
*) usage; exit 2 ;;
esac
done
if (( $# != 1 )); then
usage
exit 2
fi
input=$1
# Continue with separately quoted values, never a reconstructed command string.
printf 'config=<%s> output=<%s> verbose=%d input=<%s>\n' \
"$config" "$output" "$verbose" "$input"
The leading + in the short-option specification selects stop-at-first-operand scanning. Once INPUT begins, later strings that resemble flags remain operands. Remove the + only if the command deliberately supports options interspersed after operands, then test that contract with real invocations. Passing the original vector as "$@" is essential: getopt "$*" or an unquoted $@ has already collapsed the argument boundaries before the parser sees them.
Why the generated eval can be bounded
The util-linux manual describes quoted output as a representation that the shell can interpret again. With the long-option interface (--options/--longoptions), explicit Bash quoting, and compatibility mode disabled, the parser emits shell syntax intended to recover one positional parameter per original argument. The script then uses ordinary case, shift, and quoted expansions. It does not evaluate the option values as commands.
That assurance depends on provenance and configuration. Do not append text to parsed, interpolate any other variable into the eval string, select unquoted mode, invoke the legacy calling form, or trust a getopt binary selected through an attacker-controlled PATH. The --test probe establishes that the program advertises the enhanced interface; it does not authenticate the executable or prove it came from the expected package. In a privileged or supply-chain-sensitive launcher, resolve the utility through a trusted installation path or avoid the external parser entirely.
The environment variable GETOPT_COMPATIBLE forces compatibility behavior and disables the quoting contract, so the sample removes it before parsing. The explicit --shell bash keeps the emitted quoting flavor aligned with the interpreter. These are not optional decoration around eval: they define what syntax is generated and which shell will consume it.
Keep edge cases out of the command grammar
Use required option arguments unless an optional value materially improves the interface. In util-linux getopt, an optional argument must be attached to its option, and an empty optional argument is treated as if no argument was supplied. A required value, by contrast, can be attached or be the next argument. --config= is syntactically present but may still be semantically invalid; validate whether an empty path or value is acceptable after parsing.
Long-option abbreviations may be accepted when unambiguous. Do not advertise abbreviations as stable API: adding a new option can make a formerly valid prefix ambiguous. Document and test canonical spellings. Also decide whether an operand beginning with - requires an explicit --, and ensure the command eventually passes filenames to downstream utilities with their own end-of-options marker where supported.
The -T check is a capability probe, not an operating-system detector. A nonzero result alone is insufficient: enhanced util-linux returns exactly 4, while older or compatibility-mode implementations return 0 and print --. Other failures, such as a missing command, must remain errors rather than being mistaken for support.
Test the boundary without executing payloads
Run the script against a harmless argument recorder on the exact Linux distributions and util-linux versions you support. Include a value containing spaces, an empty string, a leading dash, a quote, a semicolon, a wildcard, a literal $(...) sequence, a backslash, and a newline. Confirm each value arrives as exactly one argument and that the literal command-substitution text is not executed. Test short and long forms, grouped short flags, a missing required value, an unknown option, --, an operand before a flag, and both values of GETOPT_COMPATIBLE before the script’s explicit unset.
Keep POSIX getopts when portability and a no-eval design are more important than long-option parsing. Choose util-linux getopt only when its dependency and generated-shell-quoting contract are explicit, versioned, and covered by integration tests. Neither parser replaces semantic validation or safe handling by the program that receives each option value.
Related:
- Portable Shell Option Parsing with getopts: State, Errors, and Reentrancy
- Bash eval: Keep Data Out of the Second Parse
Sources: