Skip to content
Lesson 14 of 14

Step 1 of 7 · Reading · ~4 min

Learn

Command Substitution and Exit Status

getopts

By the end of this lesson you can make a script accept options such as -q and -o file the standard way, handle bad usage with a clear message, and separate options from the operands that follow them. getopts is bash's built-in parser for short, single-letter options, and nearly every serious script uses it.

Positional parameters first

A script's arguments arrive as $1, $2, and so on. $# is how many there are, "$@" is all of them, and shift throws away $1 and moves the rest down one place:

set -- -q -o out.txt a.txt b.txt    # set the argument list by hand
echo "$# arguments, first is $1"    # 5 arguments, first is -q
shift
echo "$1"                           # -o

set -- replaces the argument list, which is useful for testing. On this platform a script is started with no arguments, so the exercises read a line from stdin and install it with set --. getopts cannot tell the difference.

The basics

#!/usr/bin/env bash
quiet=0
out=""

while getopts "qo:" opt; do
    case $opt in
        q) quiet=1 ;;
        o) out=$OPTARG ;;
        *) echo "usage: $0 [-q] [-o file] paths..." >&2; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

echo "quiet=$quiet out=$out paths: $*"

With the arguments -q -o out.txt a.txt b.txt this prints quiet=1 out=out.txt paths: a.txt b.txt. The pieces:

  • The option string "qo:" lists the letters. A letter followed by a colon takes a value; a bare letter is a flag.
  • getopts returns success once per option and fails when the options run out, so it is the condition of a while loop. The current letter lands in the variable you name (opt).
  • $OPTARG holds the value of an option that takes one.
  • $OPTIND is the index of the next argument to process. After the loop, shift $((OPTIND - 1)) discards the options, leaving just the operands in "$@".

What it accepts, following the POSIX rules: flags can be combined (-qo out.txt), a value may be attached (-oout.txt), and parsing stops at the first argument that is not an option. In a.txt -q, the -q is treated as an ordinary operand. A bare -- also ends the options, which is how you pass an operand that starts with a dash.

Reporting bad usage

If the option string starts with a colon, getopts runs in silent mode: it prints nothing itself, so your script decides what to say. Two special values then appear in opt:

while getopts ":qo:" opt; do
    case $opt in
        q) quiet=1 ;;
        o) out=$OPTARG ;;
        :)  echo "option -$OPTARG needs a value" >&2; exit 1 ;;
        \?) echo "unknown option -$OPTARG" >&2; exit 1 ;;
    esac
done

opt is : when a value is missing and ? for an unknown letter, and in both cases $OPTARG holds the offending letter. Match the question mark with a backslash, because an unescaped ? is a glob that matches any single character.

getopts handles only single-letter options. Long options such as --verbose need the separate GNU getopt program or a hand-written loop:

while [[ $# -gt 0 ]]; do
    case "$1" in
        --verbose|-v) verbose=1; shift ;;
        --file)       file="$2"; shift 2 ;;
        --)           shift; break ;;
        -*)           echo "unknown option: $1" >&2; exit 1 ;;
    esac
done

Interactive input: read

Scripts that prompt use read:

read -r -p "Your name: " name       # -p prints a prompt
read -r -s -p "Password: " secret   # -s does not echo what is typed
read -r -t 5 answer                 # -t gives up after 5 seconds

-r stops backslashes in the input from being treated as escapes, so write it on every read.

Common mistakes

  • Forgetting shift $((OPTIND - 1)). The options are still in "$@" when you loop over the operands.
  • No branch for ? or :. A typo in a flag is silently ignored.
  • A value-taking letter without its colon. "qo" makes -o a flag, and $OPTARG stays empty.
  • Options after operands. getopts stops at the first operand, so script file -q never sees -q.

Your exercise

Write the option parser for a small tool. The argument list reaches the script as a line on stdin and is installed for you; your job is the loop and everything after it. The hidden tests cover options in different positions, combined flags, attached values, the -- marker and both kinds of bad usage.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…