OnCallReady

Lesson 6.20 · Bash Scripting · 8 min read

getopts and a usage function

In plain words

Imagine ordering at a counter where the menu says: "-e and the name of your city (required), -t and a flavour (optional, vanilla if you do not say), -n if you only want to see the price, -h for help". The cashier reads your order left to right, one flag at a time, notes each choice, and tells you clearly if you asked for something that does not exist or forgot to say which city.

getopts ":e:t:nh" is that cashier. A letter followed by : needs a value, which arrives in $OPTARG. The leading : means "let me handle mistakes myself". After the loop, shift $((OPTIND - 1)) removes what was read, and the usage function is the printed menu: to stderr, and exit code 2 when the order was wrong.

Why this lesson

Every command you have used takes options (also called flags): ls -l, grep -c, mkdir -p. Some flags take a value (head -n 5). Your scripts will need them too: ./deploy.sh -e prod -n is clearer than remembering that $2 means "dry run". Checking $1, $2 by hand gets messy fast; bash has a builtin, getopts, that does the parsing for you.

What you need to know already: positional parameters $1 $@ $# (6.6); functions (6.15); heredocs (6.18); case (6.5); while loops (6.3).

The whole pattern

Read it once, then piece by piece below.

#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat >&2 <<'EOF'
usage: deploy.sh -e ENV [-t TAG] [-n] [-h]
  -e ENV   target environment (required)
  -t TAG   version tag (default: latest)
  -n       dry run
  -h       this help
EOF
  exit 2
}

env=""
tag="latest"
dry=0

while getopts ":e:t:nh" opt; do
  case "$opt" in
    e) env=$OPTARG ;;
    t) tag=$OPTARG ;;
    n) dry=1 ;;
    h) usage ;;
    :) echo "error: -$OPTARG needs an argument" >&2; usage ;;
    \?) echo "error: unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))      # positional arguments now start at $1

[[ -n $env ]] || { echo "error: -e is required" >&2; usage; }

Reading the optstring

":e:t:nh"
 │└┬┘└┬┘││
 │ │  │ │└─ h : no argument
 │ │  │ └── n : no argument
 │ │  └──── t : takes an argument (trailing colon)
 │ └─────── e : takes an argument
 └───────── LEADING colon: silent error mode - handle errors yourself
             with the : and \? cases instead of letting getopts print

How the loop works: each time round, getopts OPTSTRING opt reads the next option from the command line and puts its letter in $opt. When there are no options left, it exits non-zero and the while ends. The case decides what each letter does. A usage message in square brackets, [-t TAG], means "this part is optional" - a convention, not syntax.

$OPTARG holds the option's value (or, in the two error cases, the offending letter). $OPTIND is the position of the next unread argument. shift N throws away the first N positional parameters, so shift $((OPTIND - 1)) drops everything getopts consumed, and whatever is left starts at $1 again.

Conventions worth following

What you can now do

Why it helps

Every script that other people run, deploy helpers, backup tools, server setup scripts, needs a predictable interface: flags, defaults, a required check and a help message. getopts gives you that with standard behaviour, so -n -e prod, -ne prod and -eprod all work like other Unix tools, and mistakes produce a usage message instead of silently deploying with defaults.

Conventions matter in automation: usage on stderr and exit 2 lets wrappers and other scripts tell "wrong invocation" from "real failure", and -n dry runs make scripts reviewable before they touch production. Reviewing a teammate's script, a hand-rolled $1/$2 parser that breaks on argument order is a common place to suggest getopts.

Commands in this lesson

bash

FAQ

What does the leading colon in the optstring do?

It switches getopts to silent error reporting. Without it, getopts prints its own messages like "illegal option" and sets opt to ?. With it, getopts prints nothing: for an unknown option it sets opt to ? and OPTARG to the offending letter, and for a missing argument it sets opt to : and OPTARG to the option. You then print your own messages in the \?) and :) branches.

Why shift $((OPTIND - 1)) after the loop?

OPTIND is the index of the next argument getopts would process. After the loop it points at the first non-option argument. Shifting by OPTIND - 1 removes all processed options and their values, so positional arguments like file names start at $1 again. getopts stops at the first non-option argument or at --, which lets users pass values starting with a dash after --.

Can getopts handle --long-options?

No, the bash builtin only handles single-letter options. The separate getopt program from util-linux (on Ubuntu) supports long options and reorders arguments, but it is not portable to macOS's BSD getopt. For scripts you control, short options plus a clear usage message are usually enough; for larger CLIs, a language with a proper argument parser is the better tool.

How do I make an option required?

getopts has no notion of required options. Initialise the variable to empty before the loop, and after it check with [[ -n $env ]] || { echo "error: -e is required" >&2; usage; }. Validate values there too, for example with a case allowlist for environments. Doing all checks after parsing means the error messages can mention every problem clearly.

Why should usage exit with 2 and print to stderr?

Exit code 2 is the Unix convention for incorrect usage, distinct from 1 for a runtime failure, so callers and logs can tell "you called me wrong" from "the operation failed". stderr keeps the message out of stdout, where it could be captured as data by $(...) or a pipe. An explicit -h request can reasonably print usage to stdout and exit 0.

In an interview Junior

How do you parse command-line options in a bash script?

With the getopts builtin in a while loop and a case:

while getopts ":e:t:nh" opt; do
  case "$opt" in
    e) env=$OPTARG ;;
    t) tag=$OPTARG ;;
    n) dry=1 ;;
    h) usage ;;
    :) echo "error: -$OPTARG needs an argument" >&2; usage ;;
    \?) echo "error: unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))
[[ -n $env ]] || usage

The optstring lists the letters; a colon after a letter means it takes a value, delivered in $OPTARG; a leading colon means you print the errors yourself. shift $((OPTIND - 1)) drops what getopts consumed, so the remaining arguments start at $1. Conventions: usage prints to stderr and exits 2; required options are checked after the loop (getopts has no "required"); no long options - that is a separate getopt program.

Also asked: Why should a usage message go to stderr and exit 2? · What is $OPTIND, and why shift by OPTIND - 1? · What makes a script pleasant and safe for other engineers to run?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.