Bash getopts: Parse Short Options and Handle Long Options

Tested on RHEL 10.2 (Coughlan)
Package bash-5.2.26-6.el10.x86_64
util-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
Short on time?Beta Get the gist of this 14-minute read in seconds, then ask follow-up questions

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:

bash
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.sh

Run the script with a file flag and verbose:

bash
bash demo_min.sh -f test.txt -v
output
File: test.txt
Verbose

Each 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 when opt is ? or :
  • OPTIND — index of the next parameter getopts will read (starts at 1)
  • OPTERR — Bash normally sets this to 1; setting it to 0 suppresses automatic diagnostics for invalid options and missing option arguments. A leading : on the optstring also enables silent error handling without changing OPTERR

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
bash -c 'OPTIND=1; while getopts "f:" opt; do printf "opt=%s OPTARG=%s\n" "$opt" "${OPTARG-unset}"; done' -- -x 2>&1
output
bash: illegal option -- x
opt=? OPTARG=unset

Silent mode with a leading colon stores the bad letter in OPTARG:

bash
bash -c 'OPTIND=1; while getopts ":f:" opt; do printf "opt=%s OPTARG=%s\n" "$opt" "${OPTARG-unset}"; done' -- -x
output
opt=? OPTARG=x

Test missing -f under the default optstring:

bash
bash -c 'while getopts "f:" opt; do :; done' -- -f 2>&1; echo "exit=$?"
output
bash: option requires an argument -- f
exit=0

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

bash
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 -f
output
Error: -f needs a value

For unknown flags in silent mode, OPTARG holds the offending letter:

bash
bash err_colon.sh -x
output
Error: illegal flag -x

Remove Parsed Options with shift

After the while getopts loop, drop consumed parameters so $1 is the first operand:

bash
shift "$((OPTIND - 1))"

Watch OPTIND when a filename-looking token follows options. This example uses -f a, then --, then -g b:

bash
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" "$*"'
output
opt=f OPTARG=[a]
OPTIND=4
Remaining: -g b

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

  1. Parse flags with getopts.
  2. shift "$((OPTIND - 1))".
  3. Require at least one operand, or enforce exactly one, with a usage function.

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:

bash
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
}
EOF

Load the function by sourcing parse_fn.sh and call it twice; each invocation resets OPTIND locally:

bash
bash -c '. parse_fn.sh; parse_args -v -o /tmp/x; parse_args -o /tmp/y'
output
verbose=1
out=/tmp/x
out=/tmp/y

A missing -o value sets opt to : with OPTARG holding o; the : branch reports it and returns 2:

bash
bash -c '. parse_fn.sh; parse_args -o' 2>&1; echo "exit=$?"
output
missing=o
exit=2

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

text
backup.sh -v -- -weird.tar

Document 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
bash -c 'OPTIND=1; while getopts ":vd:" opt; do printf "opt=%s OPTARG=%s\n" "$opt" "$OPTARG"; done' -- --dry-run -v 2>&1
output
opt=? 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:

bash
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" "$*"
EOF

Run with mixed short and long flags:

bash
bash mixedopts.sh -v --dry-run -o backup.tar -- operand
output
verbose=1 dry_run=1 output=backup.tar operands=operand

Long-only spellings work in the same loop:

bash
bash mixedopts.sh --verbose --output=backup.tar --dry-run -- operand
output
verbose=1 dry_run=1 output=backup.tar operands=operand

Separate value form:

bash
bash mixedopts.sh --output backup.tar src
output
verbose=0 dry_run=0 output=backup.tar operands=src

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

bash
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
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" "$*"'
output
verbose=1 output=backup.tar rest=rest

An invalid option makes getopt exit non-zero, so the if ! guard stops before eval set:

bash
bash -c 'if ! opts=$(getopt -o f: -- -x 2>/dev/null); then echo "getopt failed"; exit 2; fi; eval set -- "$opts"'
output
getopt failed

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

bash
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.sh

Run with verbose, output file, and -- before a path that starts with -:

bash
bash backup.sh -v -o /var/backups/pkg.tar -- -weird-name
output
verbose=1 outfile=/var/backups/pkg.tar source=-weird-name

Missing operands trigger usage on stderr:

bash
bash backup.sh 2>&1; echo "exit=$?"
output
Usage: backup.sh [-v] [-o FILE] SOURCE
exit=2

Extra operands are rejected because usage allows only one SOURCE:

bash
bash backup.sh file1 file2 2>&1; echo "exit=$?"
output
Usage: backup.sh [-v] [-o FILE] SOURCE
exit=2

To 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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 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)