getopts is the usual way Bash scripts parse short flags (-v) and options that
take values (-f file). Search traffic clusters on bash getopts, getopts bash, bash
getopts example, and very specific questions about bash getopts leading colon
behavior missing option argument. This page is long on purpose: exact rules, a
comparison to external getopt, and subsections you can jump to once you know the
outline.
Tested all the commands and code from this article on Ubuntu 25.04, kernel 6.14.0-37-generic, Bash 5.2.37.
For plain $1 / $2 basics first, see
beginners guide to script arguments in bash.
What getopts does (and what it skips)
getopts is a shell builtin. Each call looks at the positional parameters
starting at index OPTIND, pulls the next short option from the current
cluster (for example the v in -vfname), optionally grabs its option argument
into OPTARG, bumps OPTIND, and returns status 0 while options remain.
When parsing is finished, it returns non-zero and your while loop stops.
It does not parse --long options natively, does not validate paths, and does
not remove parsed words from $@ for you—you shift after the loop.
Minimal bash getopts example
#!/usr/bin/env bash
while getopts "f:v" opt; do
case "$opt" in
f) echo "File: $OPTARG" ;;
v) echo "Verbose" ;;
esac
doneIf you save that as demo.sh and run bash demo.sh -f test.txt -v, you get:
File: test.txt
Verbosef:v—ftakes a value (:after the letter),vis a boolean flag.opt— current option letter (or?/:in error modes—covered below).OPTARG— the value forf(test.txthere).
The option string (optstring) letter by letter
The first argument to getopts is the optstring.
Flags vs option-arguments
vin the optstring →-vis allowed; no value is read intoOPTARG.f:→-fmust be followed by an option argument (next word, or text glued in the same cluster—see bundling).
Letters are typically [a-zA-Z]; pick letters that match your script’s domain.
A colon after a letter (f:) is not “silent mode”
Only a colon immediately at the start of the entire optstring changes error
reporting. A colon after f only means “-f needs an argument.”
Optional leading + (POSIX mode hint)
If the first character of the optstring is +, or if POSIXLY_CORRECT is set in
the environment, getopts can stop at the first non-option argument (POSIX
semantics). Most Bash-only scripts omit + and rely on the default Bash behavior
where options can appear among arguments unless you use -- to end options—see
below.
Variables you must know: opt, OPTARG, OPTIND, opterr
| Name | Role |
|---|---|
opt |
Your case variable—holds f, v, ?, or : depending on branch |
OPTARG |
Value for f:; or, in error branches, context (depends on leading colon—below) |
OPTIND |
Index of the next word getopts will inspect; starts at 1 |
opterr |
If non-zero (default), Bash may print diagnostics for some errors |
The while getopts …; do …; done loop
Canonical shape:
while getopts "OPTSTRING" opt; do
case "$opt" in
… ) … ;;
esac
donegetopts updates OPTIND across iterations; the loop body runs once per parsed
option (not once per argv word).
Error reporting: opterr and the leading colon
This is the section behind queries like bash getopts leading colon behavior missing argument and missing option argument.
Default: no leading colon in the optstring
Example optstring: f: (not :f:).
- Invalid option (for example
-x): Bash printsillegal option -- xto stderr,optis?, and in Bash 5.2OPTARGis empty for that case—so you only know “something failed,” not which letter, unless you parse stderr or switch to a leading colon. - Missing option argument (
-fwith nothing after it): Bash printsoption requires an argument -- f,optis?,OPTARGis empty—same?branch as invalid option. You cannot distinguish invalid vs missing fromopt/OPTARGalone without changing the optstring.
Captured run for missing -f value:
bash: option requires an argument -- f(and your case sees opt=? with empty OPTARG).
Leading colon: :f: at the start of the optstring
Prefix the whole optstring with : (example :f:v):
- Bash suppresses its own diagnostic messages for these cases (you print your own).
- Invalid option
-x:optis?, andOPTARGisx(the bad character). - Missing option argument after
-f(nothing follows the flag):optis:, andOPTARGisf(the option letter that was missing its value).
That is how you tell an invalid letter from a missing value in case.
Example loop:
#!/usr/bin/env bash
while getopts ":f:v" opt; do
case "$opt" in
f) echo "File: $OPTARG" ;;
v) echo "Verbose" ;;
:) echo "Error: -$OPTARG needs a value"; exit 1 ;;
\?) echo "Error: illegal flag -$OPTARG"; exit 1 ;;
esac
doneRunning bash demo.sh -f prints:
Error: -f needs a valueRunning bash demo.sh -x prints:
Error: illegal flag -xTruth table (Bash 5.2 behaviour)
| Situation | Optstring | Bash stderr message? | opt |
OPTARG |
|---|---|---|---|---|
Unknown flag -x |
f: |
yes | ? |
empty |
-f with missing value |
f: |
yes | ? |
empty |
Unknown flag -x |
:f: |
no | ? |
x |
-f with missing value |
:f: |
no | : |
f |
That table is what people mean by “bash getopts leading colon behavior missing
option argument”: the leading colon is what turns a missing value into opt=':'
with OPTARG holding the option letter, instead of collapsing into the same
? branch as a bad flag.
Turning off messages without a leading colon
Setting opterr=0 silences some stderr noise, but you still get opt='?' with
empty OPTARG for both invalid and missing cases—you lose the extra signal that
the leading colon gives you. Prefer an optstring like :f:… when you need clean, differentiated
errors.
Bundled short options (-vfname)
When f takes an argument, -vfmy.txt means -v plus -f with argument
my.txt:
set -- -vfmy.txt
OPTIND=1
while getopts ":vf:" opt; do
echo "opt=$opt OPTARG=[$OPTARG] OPTIND=$OPTIND"
doneOutput on this system:
opt=v OPTARG=[] OPTIND=1
opt=f OPTARG=[my.txt] OPTIND=2End of options: -- and remaining positional parameters
getopts stops when it sees -- (end of options). Anything after -- stays for
your script as normal arguments:
set -- -f a -- -g b
OPTIND=1
while getopts ":f:g:" opt; do echo "opt=$opt OPTARG=[$OPTARG]"; done
echo "OPTIND=$OPTIND"
shift $((OPTIND - 1))
echo "Remaining: $*"Output:
opt=f OPTARG=[a]
OPTIND=4
Remaining: -g bNote -g was not parsed as an option because it came after --.
shift $((OPTIND-1)) after the loop
After getopts finishes, OPTIND points at the first unprocessed word.
shift $((OPTIND-1)) drops all words that belonged to options so $1, $2, … are
your real operands (input1, input2, …).
Parsing twice or from a function: reset OPTIND
Set OPTIND=1 before a second getopts pass on the same $@, or when a
function reuses parsing logic. Otherwise the second pass starts “in the middle” and
appears broken.
OPTIND is global
OPTIND is not local to the while loop or to a function unless you declare a
separate local OPTIND in that function (advanced). In normal scripts, treat it as
global state shared with getopts.
getopts vs getopt (external)
| Topic | getopts (builtin) |
getopt (usually util-linux) |
|---|---|---|
| Availability | Bash / POSIX sh |
Separate binary; not on every minimal image |
| Long options | Not built-in | Enhanced getopt supports -l / --long |
| Whitespace / quoting | Designed for shell parsing | Historic getopt was unsafe with spaces; util-linux getopt is much better—still an external dependency |
| Typical use | Short flags in portable scripts | Reforming argv with long + short mix |
On this machine, getopt --version reports util-linux 2.40.x. A minimal getopt
example in bash might look like:
opts=$(getopt -o f:vh -- "$@") || exit 1
eval set -- "$opts"
while true; do
case "$1" in
-f) file=$2; shift 2 ;;
-v) verbose=1; shift ;;
-h) echo "help"; exit 0 ;;
--) shift; break ;;
*) break ;;
esac
done
echo "Rest: $*"Sample run after eval set -- (util-linux getopt reformats argv):
f=my.txt
v
Rest: restFor short-option-only tools, getopts stays simpler because it needs no
eval set -- step and remains a builtin.
Long options without getopt
Common patterns:
- Pre-scan:
while [[ $# -gt 0 ]]; do case $1 in --file=*) …;; --) shift; break;; esac; donethen rungetoptson the rest. - Use
getopt -lwhen you already depend on util-linux.
Practical template (strict errors + shift)
#!/usr/bin/env bash
set -euo pipefail
usage() { echo "Usage: $0 [-v] -f file [--] [args…]" >&2; exit 1; }
verbose=0
file=""
while getopts ":f:vh" opt; do
case "$opt" in
f) file=$OPTARG ;;
v) verbose=1 ;;
h) usage ;;
:) echo "Option -$OPTARG needs a value" >&2; usage ;;
\?) echo "Illegal option -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
[[ -n "$file" ]] || usage
echo "file=$file verbose=$verbose rest=$*"Example:
bash script.sh -f cfg -v a bOutput:
file=cfg verbose=1 rest=a bCommon mistakes checklist
- Forgetting
:in the optstring for options that need values. - Expecting
OPTARGon invalid options without a leading colon—often empty. - Skipping
shift $((OPTIND-1))and then treating-fas"$1". - Not resetting
OPTIND=1before a second parse. - Using
eval set -- $(getopt …)from old tutorials—prefer moderngetopt+ safe quoting, or stick togetopts.
Summary
getopts walks short options using an optstring: letters for flags,
letter: for values, and an optional leading colon on the whole string to
suppress Bash’s stderr messages while mapping missing option arguments to
opt=':' with OPTARG set to the option letter, and invalid options to
opt='?' with OPTARG set to the bad character. Without that leading colon, both
error cases tend to collapse into ? with empty OPTARG, which is why the
leading colon shows up in so many production scripts. After the loop, shift $((OPTIND-1)) leaves positional operands. Use external getopt when you
need long options or argv reshaping and can rely on util-linux; otherwise getopts
stays the clearest default for short-option parsing in bash.
Further reading:

