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:

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

output
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 experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)