Bash getopts: optstring, OPTIND, leading colon, and getopt vs getopts

Bash getopts tutorial: optstring, OPTARG, OPTIND, opterr, bash getopts leading colon behavior for missing option arguments vs invalid options, bash getopts example scripts, shift after parsing, and getopt bash vs getopts comparison.

Published

Updated

Read time 8 min read

Reviewed byDeepak Prasad

Bash getopts: optstring, OPTIND, leading colon, and getopt vs getopts

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

bash
#!/usr/bin/env bash
while getopts "f:v" opt; do
  case "$opt" in
    f) echo "File: $OPTARG" ;;
    v) echo "Verbose" ;;
  esac
done

If you save that as demo.sh and run bash demo.sh -f test.txt -v, you get:

text
File: test.txt
Verbose
  • f:vf takes a value (: after the letter), v is a boolean flag.
  • opt — current option letter (or ? / : in error modes—covered below).
  • OPTARG — the value for f (test.txt here).

The option string (optstring) letter by letter

The first argument to getopts is the optstring.

Flags vs option-arguments

  • v in the optstring → -v is allowed; no value is read into OPTARG.
  • f:-f must 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:

bash
while getopts "OPTSTRING" opt; do
  case "$opt" in
);;
  esac
done

getopts 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 prints illegal option -- x to stderr, opt is ?, and in Bash 5.2 OPTARG is 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 (-f with nothing after it): Bash prints option requires an argument -- f, opt is ?, OPTARG is empty—same ? branch as invalid option. You cannot distinguish invalid vs missing from opt/OPTARG alone without changing the optstring.

Captured run for missing -f value:

text
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: opt is ?, and OPTARG is x (the bad character).
  • Missing option argument after -f (nothing follows the flag): opt is :, and OPTARG is f (the option letter that was missing its value).

That is how you tell an invalid letter from a missing value in case.

Example loop:

bash
#!/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
done

Running bash demo.sh -f prints:

text
Error: -f needs a value

Running bash demo.sh -x prints:

text
Error: illegal flag -x

Truth 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:

bash
set -- -vfmy.txt
OPTIND=1
while getopts ":vf:" opt; do
  echo "opt=$opt OPTARG=[$OPTARG] OPTIND=$OPTIND"
done

Output on this system:

text
opt=v OPTARG=[] OPTIND=1
opt=f OPTARG=[my.txt] OPTIND=2

End of options: -- and remaining positional parameters

getopts stops when it sees -- (end of options). Anything after -- stays for your script as normal arguments:

bash
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:

text
opt=f OPTARG=[a]
OPTIND=4
Remaining: -g b

Note -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:

bash
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):

text
f=my.txt
v
Rest: rest

For short-option-only tools, getopts stays simpler because it needs no eval set -- step and remains a builtin.


Long options without getopt

Common patterns:

  1. Pre-scan: while [[ $# -gt 0 ]]; do case $1 in --file=*) …;; --) shift; break;; esac; done then run getopts on the rest.
  2. Use getopt -l when you already depend on util-linux.

Practical template (strict errors + shift)

bash
#!/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
bash script.sh -f cfg -v a b

Output:

text
file=cfg verbose=1 rest=a b

Common mistakes checklist

  • Forgetting : in the optstring for options that need values.
  • Expecting OPTARG on invalid options without a leading colon—often empty.
  • Skipping shift $((OPTIND-1)) and then treating -f as "$1".
  • Not resetting OPTIND=1 before a second parse.
  • Using eval set -- $(getopt …) from old tutorials—prefer modern getopt + safe quoting, or stick to getopts.

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:

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …