Parse Flags with getopts
Problem statement
Give a script proper options, -f FILE, -n COUNT and -v, with getopts, and handle unknown options, missing values and leftover arguments. Run it with several argument sets and print what it understood. Options make scripts flexible without depending on argument order, and getopts is built into every bash.
The script saves the tool as backup.sh and runs it with these arguments:
runs
-f db.sql -n 3-v -f db.sql extra1 extra2-vf db.sql-n 2-x-fFor each run, print the command, what it parsed (or its error), and its exit code.
Expected output:
$ backup.sh -f db.sql -n 3 file=db.sql count=3 verbose=false rest=[] -> exit 0$ backup.sh -v -f db.sql extra1 extra2 file=db.sql count=1 verbose=true rest=[extra1 extra2] -> exit 0$ backup.sh -vf db.sql file=db.sql count=1 verbose=true rest=[] -> exit 0$ backup.sh -n 2 error: -f FILE is required usage: backup.sh -f FILE [-n COUNT] [-v] [NAME...] -> exit 2$ backup.sh -x error: unknown option -x usage: backup.sh -f FILE [-n COUNT] [-v] [NAME...] -> exit 2$ backup.sh -f error: -f needs a value usage: backup.sh -f FILE [-n COUNT] [-v] [NAME...] -> exit 2Hints
while getopts ":f:n:v" opt; do case $opt in ... esac; done reads one option per loop. A letter followed by : takes a value, which arrives in $OPTARG.Approach
Optimal: getopts with case
Covers: options vs positional arguments, getopts option strings, OPTARG, OPTIND, shift $((OPTIND - 1)), quiet error mode with a leading :, combined flags like -vf, required options, why there are no long options.
Strict mode, used on every page in this section. The second line, set -euo pipefail, makes bash stop on mistakes instead of carrying on:
| Option | Means |
|---|---|
-e |
exit as soon as a command fails (with some exceptions, see Handle Command Failures) |
-u |
treat an unset variable as an error, instead of silently using empty text |
-o pipefail |
a pipeline fails if any command in it fails, not just the last one |
Put it right after the shebang line #!/usr/bin/env bash in every script you write.
Options and arguments. Options start with - and can come in any order: -n 3 -f db.sql means the same as -f db.sql -n 3. Some are switches (-v on or off), some take a value (-f db.sql). Whatever is left after the options are the positional arguments.
The option string. getopts ":f:n:v" opt describes the options:
quiet errors"]:::gray S --> F["f:
-f takes a value"]:::blue S --> N["n:
-n takes a value"]:::blue S --> V["v
-v is a switch"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
| Part | Means |
|---|---|
leading : |
quiet mode: you print the errors yourself |
f: |
-f takes a value |
n: |
-n takes a value |
v |
-v is a switch |
One option per loop. Each time round, getopts puts the next option letter in opt and its value in OPTARG, and returns false when the options run out. case handles each letter:
opt |
When | OPTARG |
|---|---|---|
f, n, v |
a known option | the value, for f and n |
? |
an unknown option like -x |
the letter x |
: |
a value is missing, as in a bare -f |
the letter f |
It also understands combined switches: -vf db.sql is -v plus -f db.sql.
Getting the rest. OPTIND is the number of the next argument to look at. When the loop ends, shift $((OPTIND - 1)) removes all the options, so "$@" holds just the leftover arguments, like extra1 extra2.
read by getopts"]:::blue ~~~ A2["extra1 extra2
left in $@ after shift"]:::green end classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style A fill:transparent,stroke:#7c3aed,stroke-width:2px
Required options. getopts does not know an option is required. Check after the loop, [[ -n $file ]] || usage, which is how -n 2 alone fails.
Walking through the code. The # Setup: lines write the tool to backup.sh, so skip past the setup part; the tool itself is the solution.
- Defaults are set first:
count=1,verbose=false. - The
while getoptsloop fills them in; unknown options and missing values print an error and callusage, which exits 2. - After
shift, the tool checks-fwas given, then prints what it parsed. tryruns each argument set with2>&1and prints the exit code.
Edge cases. getopts stops at the first argument that is not an option, so backup.sh extra -v leaves -v unparsed; options must come first. -- ends the options explicitly, so a value like -weird can follow as an argument. The value of -n is still just text; check it is a number as on the Arguments and Exit Codes page.
#!/usr/bin/env bash
set -euo pipefail
# Setup: write the tool in a fresh temporary folder
cd "$(mktemp -d)"
cat > backup.sh << 'TOOL'
#!/usr/bin/env bash
set -euo pipefail
usage() { echo "usage: ${0##*/} -f FILE [-n COUNT] [-v] [NAME...]" >&2; exit 2; }
file="" count=1 verbose=false
while getopts ":f:n:v" opt; do
case $opt in
f) file=$OPTARG ;;
n) count=$OPTARG ;;
v) verbose=true ;;
:) echo "error: -$OPTARG needs a value" >&2; usage ;;
\?) echo "error: unknown option -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
[[ -n $file ]] || { echo "error: -f FILE is required" >&2; usage; }
echo "file=$file count=$count verbose=$verbose rest=[$*]"
TOOL
try() {
echo "\$ backup.sh $*"
bash ./backup.sh "$@" 2>&1 | sed 's/^/ /' && echo " -> exit 0" || echo " -> exit ${PIPESTATUS[0]}"
}
try -f db.sql -n 3
try -v -f db.sql extra1 extra2
try -vf db.sql
try -n 2
try -x
try -fInterview follow-ups
Add --help, printing the usage to stdout and exiting 0.
Check for it before the
getoptsloop, becausegetoptswould see--helpas the unknown option-:[[ ${1:-} == --help || ${1:-} == -h ]] && { echo "usage: ..."; exit 0; }. You can also addhto the option string and ah)case, so-hworks anywhere among the options. Help asked for on purpose goes to stdout with exit 0; usage shown after a mistake goes to stderr with exit 2.
Frequently asked questions
No, bash's built-in getopts only knows single letters. You can handle a few long options by hand before the loop: a while over "$@" with case $1 in --file) file=$2; shift 2 ;; --help) ... covers most needs. The external getopt command (GNU, from util-linux) does support long options, but behaves differently on macOS and is easy to misuse. For scripts with many long options, consider whether Python's argparse would be clearer.
getopts (with an s) is a bash and POSIX built-in: always available, the same everywhere, single-letter options only. getopt is a separate program; the GNU version rearranges arguments and supports long options, while the old BSD version on macOS is limited and breaks on spaces. In portable scripts, use getopts. Only use getopt when you control the platform and really need long options.
getopts stops at the first argument that does not start with -, following the usual Unix rule that options come first. So in backup.sh notes.txt -v, -v is left in "$@" as a normal argument. Put options before arguments, or tell users so in the usage text. GNU tools often accept options anywhere because they rearrange arguments, which is why this surprises people.