| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash-5.2.26-6.el10.x86_64util-linux-2.40.2-18.el10.x86_64 (GNU getopt) |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user |
| Scope | Builtin getopts for POSIX short options (-v, -o file), OPTARG/OPTIND/OPTERR, shift after parsing, -- end-of-options, manual short and long parsing in one loop, and GNU getopt on Linux. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash getopts parses short command-line options such as -v and -o file without manually walking through $@. It stores option arguments in OPTARG and tracks parsing progress with OPTIND.
Builtin getopts does not read --verbose or --output=file. For long options, parse short and long forms together with case, or use GNU getopt when util-linux is available.
| Need | Syntax |
|---|---|
| Boolean flag | v |
| Option requiring value | o: |
| Silent errors | :vo: |
| Current option | $opt |
| Option value | $OPTARG |
| Next argument index | $OPTIND |
| Remove parsed options | shift "$((OPTIND - 1))" |
| End of options | -- |
What getopts Parses—and What It Does Not
Before writing a parser, separate four kinds of tokens on the command line:
| Kind | Example | Role |
|---|---|---|
| Option (flag) | -v |
Switch with no separate argument |
| Option argument | -o backup.tar |
Value tied to -o |
| Operand | input.tar |
Positional file or path the script acts on |
| End of options | -- |
Stops option parsing; everything after is operands |
Builtin getopts understands short options (-a, -f file, bundled -vf). It stops at -- and leaves remaining words for you to treat as script arguments. It does not natively parse GNU long options such as --dry-run or --output=value.
getopts normally stops when it reaches the first non-option argument, so conventional scripts place options before operands — for example script.sh -v file, not script.sh file -v. GNU getopt, by contrast, can normalize or intermix options differently depending on its scanning mode; util-linux documents those semantics separately.
Minimal getopts Loop
A working parser is a while loop around getopts, with a case branch per letter. Save this as demo_min.sh and run it with sample flags:
cat > demo_min.sh <<'EOF'
#!/usr/bin/env bash
while getopts "f:v" opt; do
case "$opt" in
f) printf 'File: %s\n' "$OPTARG" ;;
v) printf 'Verbose\n' ;;
esac
done
EOF
chmod +x demo_min.shRun the script with a file flag and verbose:
bash demo_min.sh -f test.txt -vFile: test.txt
VerboseEach case branch runs once per flag letter getopts consumed. After the loop, unparsed operands still sit in $1, $2, and so on until you shift.
Understand the optstring
The second argument to getopts is the optstring. It lists allowed letters and whether each letter takes an argument:
| Pattern in optstring | Meaning | Example |
|---|---|---|
v |
Flag only | -v |
o: |
Letter requires an argument | -o file or -ofile |
Leading : |
Silent error mode (see below) | :vo: |
Bundled short flags work when only the last letter may take an argument: -vfmy.txt is read as verbose plus -f with argument my.txt. There is no standard “optional argument” form in getopts; if -o may omit a value, design separate flags (-o vs -O) or handle both spellings in a manual parser.
OPTARG, OPTIND, and OPTERR
While getopts runs, Bash sets three names you read inside the loop:
OPTARG— the argument belonging to an option that requires one. In silent error mode (optstring begins with:), it also holds the offending option letter whenoptis?or:OPTIND— index of the next parametergetoptswill read (starts at1)OPTERR— Bash normally sets this to1; setting it to0suppresses automatic diagnostics for invalid options and missing option arguments. A leading:on the optstring also enables silent error handling without changingOPTERR
After options are parsed, OPTIND points at the first operand. That index is why shift "$((OPTIND - 1))" removes flags but keeps positional arguments.
Error Handling: Normal vs Silent Mode
Two different colons appear in option parsing, and they behave differently:
| Colon position | Effect |
|---|---|
Leading : on the optstring (:f:) |
Silent mode: missing required args return :; invalid letters return ? without Bash’s default stderr line |
: after a letter (f:) |
-f requires an argument |
Truth table for a required -f and unknown -x:
| Input | Default optstring f: |
Silent optstring :f: |
|---|---|---|
-f (no value) |
diagnostic; opt is ?; OPTARG unset |
no diagnostic; opt is :; OPTARG is f |
-x (unknown) |
diagnostic; opt is ?; OPTARG unset |
no diagnostic; opt is ?; OPTARG is x |
In default mode, an unknown -x prints a diagnostic and leaves OPTARG unset:
bash -c 'OPTIND=1; while getopts "f:" opt; do printf "opt=%s OPTARG=%s\n" "$opt" "${OPTARG-unset}"; done' -- -x 2>&1bash: illegal option -- x
opt=? OPTARG=unsetSilent mode with a leading colon stores the bad letter in OPTARG:
bash -c 'OPTIND=1; while getopts ":f:" opt; do printf "opt=%s OPTARG=%s\n" "$opt" "${OPTARG-unset}"; done' -- -xopt=? OPTARG=xTest missing -f under the default optstring:
bash -c 'while getopts "f:" opt; do :; done' -- -f 2>&1; echo "exit=$?"bash: option requires an argument -- f
exit=0An invalid option or missing required argument does not automatically make your script exit non-zero. getopts reports the condition through opt and OPTARG; your case branch should print the appropriate message and exit or return non-zero when the CLI input is invalid.
With a leading colon and explicit handling:
cat > err_colon.sh <<'EOF'
#!/usr/bin/env bash
while getopts ":f:v" opt; do
case "$opt" in
f) printf 'File: %s\n' "$OPTARG" ;;
v) printf 'Verbose\n' ;;
:) printf 'Error: -%s needs a value\n' "$OPTARG" ;;
\?) printf 'Error: illegal flag -%s\n' "$OPTARG" ;;
esac
done
EOF
bash err_colon.sh -fError: -f needs a valueFor unknown flags in silent mode, OPTARG holds the offending letter:
bash err_colon.sh -xError: illegal flag -xRemove Parsed Options with shift
After the while getopts loop, drop consumed parameters so $1 is the first operand:
shift "$((OPTIND - 1))"Watch OPTIND when a filename-looking token follows options. This example uses -f a, then --, then -g b:
bash -c 'set -- -f a -- -g b; OPTIND=1; while getopts ":f:g:" opt; do printf "opt=%s OPTARG=[%s]\n" "$opt" "$OPTARG"; done; printf "OPTIND=%s\n" "$OPTIND"; shift $((OPTIND - 1)); printf "Remaining: %s\n" "$*"'opt=f OPTARG=[a]
OPTIND=4
Remaining: -g bgetopts stopped at --; it did not treat -g as an option. After shift, $1 is -g and $2 is b — operands you can pass to tar, cp, or other commands.
Validate Remaining Operands
Option parsing and operand validation are separate steps. A common pattern:
- Parse flags with
getopts. shift "$((OPTIND - 1))".- Require at least one operand, or enforce exactly one, with a
usagefunction.
If the script needs a single source path and none remain after the shift, print usage to stderr and exit with a non-zero exit code (many CLIs use 2 for usage errors).
Parse Options Inside a Reusable Function
OPTIND is global. Calling getopts twice in one script without resetting it makes the second call skip arguments. Inside a Bash function, set local OPTIND=1 before the loop:
cat > parse_fn.sh <<'EOF'
#!/usr/bin/env bash
parse_args() {
local OPTIND=1 OPTARG opt
while getopts ":vo:" opt; do
case "$opt" in
v) printf 'verbose=1\n' ;;
o) printf 'out=%s\n' "$OPTARG" ;;
:) printf 'missing=%s\n' "$OPTARG" >&2; return 2 ;;
\?) printf 'bad=%s\n' "$OPTARG" >&2; return 2 ;;
esac
done
}
EOFLoad the function by sourcing parse_fn.sh and call it twice; each invocation resets OPTIND locally:
bash -c '. parse_fn.sh; parse_args -v -o /tmp/x; parse_args -o /tmp/y'verbose=1
out=/tmp/x
out=/tmp/yA missing -o value sets opt to : with OPTARG holding o; the : branch reports it and returns 2:
bash -c '. parse_fn.sh; parse_args -o' 2>&1; echo "exit=$?"missing=o
exit=2The second successful call starts fresh because local OPTIND=1 runs at the top of each invocation.
Handle -- End of Options
-- conventionally marks the end of options. Bash getopts recognizes it, allowing a later operand that begins with - to remain positional.
For a file literally named -weird.tar, callers run:
backup.sh -v -- -weird.tarDocument that form in usage text when your script accepts arbitrary pathnames.
Does Bash getopts Support Long Options?
No. Builtin getopts only parses short single-letter options defined in your optstring. A token like --dry-run is not a long option to getopts; it is treated as a cluster of short flags. Running getopts on --dry-run -v mis-parses the word instead of preserving it for a later pass:
bash -c 'OPTIND=1; while getopts ":vd:" opt; do printf "opt=%s OPTARG=%s\n" "$opt" "$OPTARG"; done' -- --dry-run -v 2>&1opt=? OPTARG=-
opt=d OPTARG=ry-run
opt=v OPTARG=Because of that behavior, do not run getopts first and plan to handle long options in a second loop unless you deliberately require short options before long ones (an unusual ordering rule).
Choose the parser based on the option syntax you need:
| Goal | Approach |
|---|---|
| Short options only | getopts |
| Short and long options without external tools | One Bash while / case parser for -v, --verbose, -o, --output, and -- |
| Short and long options on GNU/Linux | GNU getopt with -o and --long, then walk the normalized $@ |
Mixing getopts with a later long-option pass breaks normal argument order such as script.sh --dry-run -v file.
Parse Short and Long Options Manually with case
When your CLI needs both -v and --verbose, handle every spelling in one while/case loop with a case statement — not in a separate pass after getopts:
cat > mixedopts.sh <<'EOF'
#!/usr/bin/env bash
verbose=0
dry_run=0
output=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
verbose=1
shift
;;
--dry-run)
dry_run=1
shift
;;
-o|--output)
(($# >= 2)) || {
printf 'Missing value for %s\n' "$1" >&2
exit 2
}
output=$2
shift 2
;;
--output=*)
output=${1#*=}
shift
;;
--) shift; break ;;
--*) printf 'Unknown: %s\n' "$1" >&2; exit 2 ;;
*) break ;;
esac
done
printf 'verbose=%s dry_run=%s output=%s operands=%s\n' "$verbose" "$dry_run" "$output" "$*"
EOFRun with mixed short and long flags:
bash mixedopts.sh -v --dry-run -o backup.tar -- operandverbose=1 dry_run=1 output=backup.tar operands=operandLong-only spellings work in the same loop:
bash mixedopts.sh --verbose --output=backup.tar --dry-run -- operandverbose=1 dry_run=1 output=backup.tar operands=operandSeparate value form:
bash mixedopts.sh --output backup.tar srcverbose=0 dry_run=0 output=backup.tar operands=srcThis parser stops at the first operand, so place options before positional arguments unless you deliberately implement option permutation. GNU util-linux getopt differs here: in its normal enhanced mode it can rearrange non-options after options, while POSIX-style scanning stops at the first non-option.
Handle unknown options and missing values in the case branches; print errors to stderr and exit non-zero when the arguments are invalid. The ($# >= 2) check distinguishes a missing argument from an empty one — --output= is accepted here because an argument was supplied. To forbid an empty output path, validate output separately with [[ -n $output ]].
GNU getopt vs Bash getopts
Bash getopts |
GNU getopt (util-linux) |
|
|---|---|---|
| Implementation | Shell builtin in Bash | External util-linux program |
| Long options | No | Yes (--long) |
| Portability | POSIX-standard interface; available in POSIX shells | GNU/util-linux behavior; not portable to all getopt implementations |
| Typical use | Short flags in portable scripts | Normalized short and long flags on Linux |
The examples below use GNU getopt from util-linux. Check whether getopt succeeded before reloading parameters:
if ! opts=$(getopt -o o:vh --long output:,verbose,help -- "$@"); then
exit 2
fi
eval set -- "$opts"GNU getopt returns shell-quoted text, which is why the documented pattern uses eval set -- "$opts". Keep "$@" quoted when calling getopt and keep "$opts" quoted in the eval command; removing those protections can corrupt arguments that contain spaces or shell metacharacters.
Walk short and long names from the normalized list:
bash -c 'if ! opts=$(getopt -o o:vh --long output:,verbose,help -- --output=backup.tar --verbose rest); then exit 2; fi; eval set -- "$opts"; verbose=0 output=""; while true; do case "$1" in -o|--output) output="$2"; shift 2;; -v|--verbose) verbose=1; shift;; -h|--help) printf "help\n"; shift;; --) shift; break;; *) break;; esac; done; printf "verbose=%s output=%s rest=%s\n" "$verbose" "$output" "$*"'verbose=1 output=backup.tar rest=restAn invalid option makes getopt exit non-zero, so the if ! guard stops before eval set:
bash -c 'if ! opts=$(getopt -o f: -- -x 2>/dev/null); then echo "getopt failed"; exit 2; fi; eval set -- "$opts"'getopt failedOperands after -- in the getopt output land in $@ for your main logic.
Build a Complete Short-Option Parser with getopts
This backup.sh is a complete short-option parser: silent getopts, operand validation, -- support, help on exit 0, and usage mistakes on exit 2:
cat > backup.sh <<'EOF'
#!/usr/bin/env bash
usage() {
printf 'Usage: %s [-v] [-o FILE] SOURCE\n' "$0" >&2
exit 2
}
verbose=0
outfile=""
while getopts ":vo:h" opt; do
case "$opt" in
v) verbose=1 ;;
o) outfile=$OPTARG ;;
h) printf 'Usage: %s [-v] [-o FILE] SOURCE\n' "$0"; exit 0 ;;
:) printf 'Option -%s needs a value\n' "$OPTARG" >&2; usage ;;
\?) printf 'Illegal option -%s\n' "$OPTARG" >&2; usage ;;
esac
done
shift "$((OPTIND - 1))"
[[ $# -eq 1 ]] || usage
printf 'verbose=%s outfile=%s source=%s\n' "$verbose" "$outfile" "$1"
EOF
chmod +x backup.shRun with verbose, output file, and -- before a path that starts with -:
bash backup.sh -v -o /var/backups/pkg.tar -- -weird-nameverbose=1 outfile=/var/backups/pkg.tar source=-weird-nameMissing operands trigger usage on stderr:
bash backup.sh 2>&1; echo "exit=$?"Usage: backup.sh [-v] [-o FILE] SOURCE
exit=2Extra operands are rejected because usage allows only one SOURCE:
bash backup.sh file1 file2 2>&1; echo "exit=$?"Usage: backup.sh [-v] [-o FILE] SOURCE
exit=2To add --dry-run, extend the manual case parser above or GNU getopt with --long dry-run,verbose — not a second loop after getopts.
Exit conventions are interface choices, not Bash rules:
| Situation | Typical exit | Stream |
|---|---|---|
--help / -h |
0 |
stdout |
| Unknown flag or missing operand | 2 (sometimes 1) |
stderr |
| Runtime failure | 1 or higher |
stderr |
Pick one scheme per script and document it in usage text.
Common Parsing Bugs
| Symptom | Likely cause | Fix |
|---|---|---|
| First operand eaten or skipped | Forgot shift "$((OPTIND - 1))" |
Shift after the getopts loop |
| Second function call ignores flags | Stale global OPTIND |
local OPTIND=1 at function start |
-f without value mishandled |
No : branch in silent mode |
Use leading : in optstring; handle : in case |
--dry-run parsed as short flags |
getopts run before long-option handling |
Use one manual loop or GNU getopt; do not two-pass with getopts first |
File -data.tar parsed as flags |
No -- support documented |
Accept -- and pass it through to getopts |
| Word splitting on paths | Unquoted $OPTARG or $1 |
Quote "$OPTARG" and "$1" |
eval set corrupts arguments |
Unquoted "$opts" or "$@" |
Quote both; check getopt exit status before eval |
Summary
Bash getopts is the right default when your CLI only needs short flags: declare an optstring, branch on each letter in case, read option arguments from OPTARG, then shift "$((OPTIND - 1))" so operands line up in $1, $2, and so on. Leading : in the optstring switches to silent errors so you control messages for missing arguments (:) and bad letters (?); in default mode, OPTARG stays unset for ? errors.
Builtin getopts does not parse --long options and will mis-read tokens like --dry-run as short flags. For mixed short and long names, use one manual while/case loop or GNU getopt with --long — not getopts followed by a separate long-option pass unless you intentionally restrict argument order. Reset OPTIND with local OPTIND=1 inside functions, document -- for paths that start with -, and separate help (exit 0) from usage errors (often exit 2).
The backup.sh pattern covers short-option parsing end to end. When you need long flags or a fuller script layout, extend the manual parser or GNU getopt sections above rather than bolting a second pass onto getopts.
References
Frequently Asked Questions
Does getopts support --long options?
No. Builtin getopts only handles short single-character options listed in the optstring. Long options require a manual case loop that handles short and long forms together, or external GNU getopt.
What is the difference between getopts and getopt?
getopts is a shell builtin standardized by POSIX for short options. getopt is an external program; GNU util-linux getopt supports long options but is not interchangeable with BSD or legacy getopt on every system.
What does a leading colon mean in the optstring?
A leading : enables silent mode. Missing required arguments set opt to : with OPTARG holding the option letter. Invalid options set opt to ? with OPTARG holding the bad letter, without Bash printing its default diagnostic first.
Why must I reset OPTIND?
OPTIND is global and remembers where the last getopts loop stopped. A second parse in the same shell without OPTIND=1 skips arguments. Functions should use local OPTIND=1 before calling getopts.
Why do I shift by OPTIND - 1?
OPTIND is the index of the next unparsed parameter. Subtracting one removes only the option words getopts already consumed, leaving operands in $1 onward.

