opscape

Shell builtins · bash builtin

getopts

Parse short command-line options such as -v and -o FILE in a script, one option per call, inside a while loop.

getopts OPTSTRING NAME [ARG...]

8 of 8 examples run in Debian 13.7 on 2026-10-31 · manual: bash(1), SHELL BUILTIN COMMANDS

getopts is the shell's built-in option parser. You describe the options in an option string: each letter is an option, and a letter followed by : takes an argument. Called in a while loop, each call puts the next option letter into a variable, its argument (if any) into OPTARG, and returns non-zero when the options run out. OPTIND is left pointing at the first non-option argument, so shift $((OPTIND - 1)) leaves only the operands in $@.

It handles the details users expect: bundled flags (-vn), arguments attached or separate (-ofile or -o file), and -- to end options. A leading : in the option string turns on silent mode, where getopts prints no messages and instead reports problems through the variable (? for an unknown option, : for a missing argument) so the script can word its own errors.

What it does not do: long options like --verbose, and options after the first operand (script file -v stops at file). For long options either parse by hand with a case loop over $1 and shift, or use the external getopt from util-linux. getopts is POSIX and works the same in dash.

Examples

A complete script with getopts

backup.sh
#!/usr/bin/env bash
usage() { echo "usage: ${0##*/} [-v] [-n] [-o DIR] SOURCE..." >&2; exit 2; }

verbose=0 dry_run=0 outdir=.
while getopts ':vno:h' opt; do
  case $opt in
    v) verbose=1 ;;
    n) dry_run=1 ;;
    o) outdir=$OPTARG ;;
    h) usage ;;
    :) echo "error: -$OPTARG needs an argument" >&2; usage ;;
    ?) echo "error: unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))
(( $# > 0 )) || usage

echo "verbose=$verbose dry_run=$dry_run outdir=$outdir sources=($*)"
chmod +x backup.sh
./backup.sh -v -o /tmp/out data logs
./backup.sh data
output
verbose=1 dry_run=0 outdir=/tmp/out sources=(data logs)
verbose=0 dry_run=0 outdir=. sources=(data)

The option string :vno:h declares -v, -n and -h as flags and -o as taking an argument; the leading : selects silent error handling. After the loop, shift removes the parsed options so $@ holds only the sources. With no options the loop body never runs and the defaults stand.

Bundled flags, attached arguments and --

backup.sh
#!/usr/bin/env bash
usage() { echo "usage: ${0##*/} [-v] [-n] [-o DIR] SOURCE..." >&2; exit 2; }

verbose=0 dry_run=0 outdir=.
while getopts ':vno:h' opt; do
  case $opt in
    v) verbose=1 ;;
    n) dry_run=1 ;;
    o) outdir=$OPTARG ;;
    h) usage ;;
    :) echo "error: -$OPTARG needs an argument" >&2; usage ;;
    ?) echo "error: unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))
(( $# > 0 )) || usage

echo "verbose=$verbose dry_run=$dry_run outdir=$outdir sources=($*)"
bash backup.sh -vn -o/srv/backups data
bash backup.sh -n -o "dir with spaces" data
bash backup.sh -v -- -weird-dir data
output
verbose=1 dry_run=1 outdir=/srv/backups sources=(data)
verbose=0 dry_run=1 outdir=dir with spaces sources=(data)
verbose=1 dry_run=0 outdir=. sources=(-weird-dir data)

-vn is split into -v and -n, and -o/srv/backups is -o with an attached argument. A quoted argument with spaces arrives intact in OPTARG. -- ends the options and is consumed, so -weird-dir is treated as a source.

Unknown options, missing arguments and no operands

backup.sh
#!/usr/bin/env bash
usage() { echo "usage: ${0##*/} [-v] [-n] [-o DIR] SOURCE..." >&2; exit 2; }

verbose=0 dry_run=0 outdir=.
while getopts ':vno:h' opt; do
  case $opt in
    v) verbose=1 ;;
    n) dry_run=1 ;;
    o) outdir=$OPTARG ;;
    h) usage ;;
    :) echo "error: -$OPTARG needs an argument" >&2; usage ;;
    ?) echo "error: unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))
(( $# > 0 )) || usage

echo "verbose=$verbose dry_run=$dry_run outdir=$outdir sources=($*)"
bash backup.sh -x data;  echo "exit $?"
bash backup.sh data -o;  echo "exit $?"
bash backup.sh -v -o;    echo "exit $?"
bash backup.sh -v;       echo "exit $?"
output
error: unknown option -x
usage: backup.sh [-v] [-n] [-o DIR] SOURCE...
exit 2
verbose=0 dry_run=0 outdir=. sources=(data -o)
exit 0
error: -o needs an argument
usage: backup.sh [-v] [-n] [-o DIR] SOURCE...
exit 2
usage: backup.sh [-v] [-n] [-o DIR] SOURCE...
exit 2

In silent mode an unknown option sets opt to ? and a missing argument sets it to :, with the letter in OPTARG either way, so the script words its own errors. The second line is a trap: parsing stops at the first operand, data, so -o is never seen as an option and is accepted as a second source. The last call has options but no sources, so the (( $# > 0 )) check prints the usage line.

Default error messages without the leading colon

set -- -a -z -b
while getopts 'ab:' opt; do echo "opt=$opt OPTARG=${OPTARG-unset}"; done
output
opt=a OPTARG=unset
/bin/bash: illegal option -- z
opt=? OPTARG=unset
/bin/bash: option requires an argument -- b
opt=? OPTARG=unset

Without the leading :, getopts prints its own message for the unknown -z and for -b, which needs an argument and has none. Both set opt to ? and leave OPTARG unset, so the script cannot tell the two cases apart. Silent mode fixes both problems.

Collect a repeatable option and count a flag

set -- -v -v -e ERROR -e WARN logs/app.log
verbosity=0 patterns=()
while getopts 've:' opt; do
  case $opt in
    v) verbosity=$((verbosity + 1)) ;;
    e) patterns+=("$OPTARG") ;;
  esac
done
shift $((OPTIND - 1))
echo "verbosity=$verbosity patterns=${patterns[*]} file=$1"
output
verbosity=2 patterns=ERROR WARN file=logs/app.log

getopts reports an option each time it appears, so a counter gives -v -v (or -vv) a level, and appending to an array collects every -e. This mirrors how grep -e and ssh -v behave.

Watch OPTIND advance

set -- -v -o out.txt input.csv
while getopts 'vo:' opt; do echo "got -$opt, OPTIND now $OPTIND"; done
shift $((OPTIND - 1))
echo "remaining: $*"
output
got -v, OPTIND now 2
got -o, OPTIND now 4
remaining: input.csv

OPTIND is the index of the next argument to look at. -o out.txt consumes two arguments, so it jumps from 2 to 4. shift $((OPTIND - 1)) drops exactly the parsed options.

The same loop works in sh

set -- -q -n 3 file.txt
quiet=no count=1
while getopts 'qn:' opt; do
  case $opt in
    q) quiet=yes ;;
    n) count=$OPTARG ;;
    *) exit 2 ;;
  esac
done
shift $((OPTIND - 1))
echo "quiet=$quiet count=$count file=$1"
output
quiet=yes count=3 file=file.txt

This runs under dash, the /bin/sh on Debian and Ubuntu. getopts is POSIX, so option parsing is one of the few areas where a #!/bin/sh script gives up nothing compared to bash.

Reset OPTIND when using getopts in a function

f() {
  local OPTIND opt
  while getopts 'q' opt; do echo "  option -$opt"; done
  shift $((OPTIND - 1)); echo "  args: $*"
}
echo "first call:";  f -q one
echo "second call:"; f -q two
output
first call:
  option -q
  args: one
second call:
  option -q
  args: two

OPTIND is a global that getopts does not reset. Making it local gives each call a fresh start; without it, the second call would begin at index 2 and miss -q.

Common pitfalls

  • Forgetting shift $((OPTIND - 1)) after the loop: $1 is still the first option, not the first file.

  • Options after operands are not parsed: script file -v treats -v as an operand. Document that options come first, or parse by hand.

  • Reusing getopts in a function or a second loop without resetting OPTIND=1 (or making it local): parsing starts where the last loop left off.

  • getopts has no long options. Do not confuse it with getopt (no s), an external util-linux program that supports --long options but needs careful quoting with eval set --.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • getopts is POSIX and identical in bash, dash, zsh and ksh, on Linux and macOS. The external getopt differs: the util-linux one on Linux supports long options, the BSD/macOS one does not and mangles arguments with spaces.

Related