# Options and Flags: getopts and --help

getopts reads a script's one-letter options in any order, flags and options with a value alike, and a real tool also answers --help with a help page and --version with its version number.

Journeyman · Unix · Linux · Updated November 10, 2026

## After this lesson you can

- Read a script's one-letter options in any order with while getopts, taking an option's value from $OPTARG
- Answer an unknown option or a missing value with a message on stderr and exit code 2, using the : and ? cases of getopts
- Give a script a --help page with a here document and a --version line, checked before getopts runs

## Key takeaways

- A flag switches something on (-q). An option with a value takes the word after it (-c dinner). Both work in any order, or joined (-qc dinner).
- while getopts ":c:q" opt reads the options one at a time. A colon after a letter means it takes a value, which arrives in $OPTARG.
- A colon at the start of the letters lets you word the errors: getopts gives : for a missing value and ? for an unknown option.
- getopts reads only one-letter options, so check "$1" for --help and --version before its loop.
- A help page is a here document: cat <<EOF, the lines of text, then EOF on a line of its own.

Module 5 said that a script is an application. This chapter is where that pays off. By its end, `recipes`, a recipe box you write yourself, takes options, has commands such as `recipes add`, keeps its data where real apps keep theirs, and prints tidy columns in color: the shape of `git`, `apt` and `spend`, the expense tracker you build after this module.

First, options. You've typed `ls -la`, `sort -rn` and `tar -czf trip.tar.gz trip`: letters after a dash, in any order, some joined, and some (`tar`'s `-f`) followed by a value. A recipe box needs the same: only dinners, only quick ones, or both. `case "$1"`, from *Menus and Choices: case and select*, looks at one word, but `-q -c dinner`, `-c dinner -q` and `-qc dinner` all mean the same thing, and checking every order by hand soon gets out of hand. Bash has a command for it: `getopts` (read "get opts").

## Flags, and options with a value

Ordering coffee, "iced" is on or off, but "milk" needs an answer, such as oat. A **flag** is the on-or-off kind: `-q`, "quick ones only". An **option with a value** takes the next word: `-c dinner`.

Make the practice folder. Each line of `recipes.txt` is a recipe's minutes, category and name:

```bash
mkdir -p ~/terminal-practice/recipe-app && cd ~/terminal-practice/recipe-app
printf '%s\n' '25 dinner Taco Tuesday' '5 breakfast Overnight Oats' '45 dinner Slow Cooker Chili' > recipes.txt
printf '%s\n' '60 dessert Banana Bread' '20 lunch Tomato Soup' '15 breakfast Blueberry Pancakes' >> recipes.txt
```

Write the script with `nano recipes`, then `chmod +x recipes`:

```bash
#!/usr/bin/env bash
# recipes: lists the recipes in recipes.txt

while read -r minutes category name; do
  echo "$name ($minutes min, $category)"
done < recipes.txt
```

`./recipes` lists all six, from `Taco Tuesday (25 min, dinner)` down; `read`'s last variable gets the rest of the line.

## getopts reads them one at a time

Replace the lines below the comment with these:

```bash
only=""
quick=no

while getopts "c:q" opt; do
  case "$opt" in
    c) only="$OPTARG" ;;
    q) quick=yes ;;
  esac
done

while read -r minutes category name; do
  if [[ -n "$only" && "$category" != "$only" ]]; then
    continue
  fi
  if [[ "$quick" == yes && "$minutes" -gt 30 ]]; then
    continue
  fi
  echo "$name ($minutes min, $category)"
done < recipes.txt
```

Read `while getopts "c:q" opt` as "while there's another option, put its letter in `opt`". `"c:q"` lists the letters it accepts. The colon after `c` means "`c` takes a value", and getopts puts that value in `OPTARG`. The `case` gives each letter its job:

```bash
./recipes -c dinner
./recipes -qc breakfast
```

```text
Taco Tuesday (25 min, dinner)
Slow Cooker Chili (45 min, dinner)
Overnight Oats (5 min, breakfast)
Blueberry Pancakes (15 min, breakfast)
```

`-q -c breakfast` and `-c breakfast -q` give the same breakfasts.

## When someone types it wrong

```bash
./recipes -x
```

```text
./recipes: illegal option -- x
Taco Tuesday (25 min, dinner)
Overnight Oats (5 min, breakfast)
Slow Cooker Chili (45 min, dinner)
Banana Bread (60 min, dessert)
Tomato Soup (20 min, lunch)
Blueberry Pancakes (15 min, breakfast)
```

That's bash's message, not yours, and the script listed everything anyway. Put a colon at the start of the letters, `":c:q"`, and getopts stays quiet: it puts `?` in `opt` for an unknown letter and `:` for a missing value, with the letter in `OPTARG`. Add two choices to the `case`:

```bash
    :) echo "recipes: -$OPTARG needs a value, such as -$OPTARG dinner" >&2; exit 2 ;;
    \?) echo "recipes: there's no -$OPTARG option. Try: recipes --help" >&2; exit 2 ;;
```

`>&2` (read "into stream 2") sends the message to standard error, as in *Errors a User Can Act On*. The `\` before `?` matters: a bare `?` pattern matches any one character, so it would catch every letter. Exit code 2 is the usual "you ran me wrong", as in `grep` and `ls`.

```bash
./recipes -x
./recipes -c
```

```text
recipes: there's no -x option. Try: recipes --help
recipes: -c needs a value, such as -c dinner
```

## A help page and a version

Every real tool answers `--help`. A **here document** writes a help page as it will look: `cat <<EOF` (read "cat, here's the text, up to EOF"), the lines, then `EOF` alone on a line. `EOF` is a made-up end word. Put this under the comment line:

```bash
usage() {
  cat <<EOF
Usage: recipes [-q] [-c CATEGORY]
List the recipes in your recipe box.

  -c CATEGORY   only recipes in CATEGORY, such as dinner
  -q            only quick recipes: 30 minutes or less
  -h, --help    show this help
  --version     show the version
EOF
}

case "$1" in
  --help) usage; exit 0 ;;
  --version) echo "recipes 1.0.0"; exit 0 ;;
esac
```

Add `h) usage; exit 0 ;;` to the getopts `case`, and an `h` to its letters: `":c:qh"`. Now `./recipes --help` and `./recipes -h` print the page, and `./recipes --version` prints `recipes 1.0.0`. Asking for help isn't a mistake, so that's exit 0.

getopts reads only one-letter options. Given `--quick`, it answers `there's no -- option`, a real puzzle, so `--help` and `--version` are checked before its loop. In 1.0.0, the last number goes up for a fix, the middle for a new feature, the first for a change that breaks old habits. (zsh has the same `getopts`. `getopt`, without the s, is a different program.)

## Check yourself

2 questions to try on the lesson page.

## So what

Your script now reads options the way `ls` and `tar` do, refuses typos clearly and explains itself on `--help`: the first things anyone looks for in a real tool.

## Key terms

- [Flags and Options](https://www.ask-codey.com/glossary/flags-and-options)
- [getopts](https://www.ask-codey.com/glossary/getopts)
- [Here Document](https://www.ask-codey.com/glossary/here-document)
- [Standard Streams](https://www.ask-codey.com/glossary/standard-streams)

## Cheat sheets

- [Options and Flags: getopts and --help](https://www.ask-codey.com/cheat-sheets/bash-options-with-getopts-cheat-sheet)

## Related

- [Menus and Choices: case and select](https://www.ask-codey.com/lessons/bash-case-and-select)
- [Arguments: $1, $# and "$@"](https://www.ask-codey.com/lessons/bash-script-arguments)
- [Subcommands: app add, app list](https://www.ask-codey.com/lessons/bash-subcommands)

Source: https://www.ask-codey.com/lessons/bash-options-with-getopts
