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

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:

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:

#!/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:

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:

./recipes -c dinner
./recipes -qc breakfast
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

./recipes -x
./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:

    :) 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.

./recipes -x
./recipes -c
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:

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

Cheat sheets

Related