In bash, what changes when a getopts optstring begins with a colon, as in `while getopts ":f:v" opt`, compared with `"f:v"` — and how does the script then tell an unknown option from a missing option value?
answer
- the first character of the optstring is special
- it changes who prints the error
- two failures, two different signals
- one branch for unknown, one for missing
- the offending letter still reaches you
basics
~20 sA leading colon puts getopts in silent error mode: bash stops printing its own diagnostics, an unknown option sets the variable to ? and a missing value sets it to :, with the offending option letter in OPTARG so the script can report both itself.
solid answer
~50 sA colon as the *first* character of the optstring switches getopts into silent error reporting. Without it, bash writes its own diagnostic to stderr and sets your variable to `?` for both an unknown option and a missing required value, leaving `OPTARG` unset — so you cannot tell the two apart or control the wording. With the leading colon, bash prints nothing, and the cases separate: an invalid option sets the variable to `?`, a missing value sets it to `:`, and in both cases `OPTARG` holds the offending option letter. You then handle them in the case block with `\?)` and `:)` branches, printing your own message with the program name and exiting with a usage status. It also means every message about your script's interface comes from your script, which matters when the output is being read out of a log.
code
bash · 21 lines#!/usr/bin/env bash
# Default mode: bash writes the diagnostic, both failures look the same.
while getopts "f:v" opt; do
case $opt in
f) file=$OPTARG ;;
v) verbose=1 ;;
\?) echo "bad usage" >&2; exit 2 ;; # also reached for a missing value
esac
done
# Silent mode: the script owns every message, and the cases separate.
OPTIND=1
while getopts ":f:v" opt; do
case $opt in
f) file=$OPTARG ;;
v) verbose=1 ;;
:) printf '%s: option -%s requires a value\n' "${0##*/}" "$OPTARG" >&2; exit 2 ;;
\?) printf '%s: unknown option -%s\n' "${0##*/}" "$OPTARG" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))go deeper
Know the shape: a colon at the front of the optstring means your script prints the errors instead of bash, and you add : and \? branches to the case.
State the four combinations precisely — which variable value and what OPTARG holds for an unknown option and for a missing value, in each mode.
Argue the operational point: a tool other people run in CI should own every message about its own command line, and a mistyped flag must exit rather than fall through to defaults.
Decide the estate-wide convention for how tools report interface errors — wording, stream, exit status — so failures are machine-greppable and consistent across scripts written by different teams.
## Two error-reporting modes `getopts` has two ways of dealing with an argument it cannot accept, selected by whether the optstring starts with a colon. **Verbose (default)** — optstring `f:v`: - Unknown option: your variable is set to `?`, `OPTARG` is unset, and bash writes a diagnostic to standard error. - Missing required value: your variable is set to `?`, `OPTARG` is unset, and bash writes a diagnostic. **Silent** — optstring `:f:v`: - Unknown option: your variable is set to `?`, `OPTARG` is set to the offending option character, nothing is printed. - Missing required value: your variable is set to `:`, `OPTARG` is set to the option character, nothing is printed. The leading colon is not an option named `:`; it is a mode switch, and it must be the very first character. ## Why the distinction matters In verbose mode both failures look identical inside your `case`, so the best you can do is a generic "bad usage" message on top of the one bash already printed — two messages for one mistake, one of them worded by the shell and not by you. Silent mode gives you two separate branches and the letter involved, so you can say exactly what went wrong: ```bash while getopts ":f:v" opt; do case $opt in f) file=$OPTARG ;; v) verbose=1 ;; :) printf '%s: option -%s requires a value\n' "${0##*/}" "$OPTARG" >&2; exit 2 ;; \?) printf '%s: unknown option -%s\n' "${0##*/}" "$OPTARG" >&2; exit 2 ;; esac done shift $((OPTIND - 1)) ``` `\?` is escaped in the case branch because an unescaped `?` would be read as a pattern rather than a literal question mark. ## What getopts returns on an error Critically, an invalid option is **not** the end of options. getopts returns success and the loop keeps going; only running out of arguments (or reaching an operand or `--`) ends it. So if your `case` has no `\?)` branch, a mistyped flag is reported — or silently swallowed, in silent mode — and the script carries on with its defaults. Neither `set -e` nor `set -u` catches this, because nothing failed and nothing was unset. Handling `\?` explicitly and exiting is what turns a typo into a refusal. ## A missing value at the end of the line `script -f` with nothing after it is the missing-value case. Note what silent mode does *not* do: it does not invent an empty `OPTARG` for `-f`. It reports the `:` case with `OPTARG=f`, and `file` keeps whatever value it had before the loop. If you skipped the `:` branch, the script would run with an unset or default `file` and no explanation anywhere. ## OPTERR, and why it is not the same thing Setting the shell variable `OPTERR` to 0 also suppresses bash's diagnostics, even when the optstring has no leading colon. But it only silences the message — it does not give you the `:` case or populate `OPTARG`, so you still cannot distinguish the two failures. Prefer the leading colon; reach for `OPTERR` only when you are stuck with an optstring you cannot change. ## Portability note The leading colon and the `:`/`?` split are specified by POSIX, so this behaviour is the same in dash, ash and ksh, not a bashism. Bash's exact diagnostic wording in verbose mode is an implementation detail — never parse it, and never let it be the only user-visible error your tool produces. ## Putting it together The pattern to reach for by default in any script other people run: leading colon, explicit `:` and `\?` branches, message on stderr with the program name and the offending letter, usage text, and a non-zero exit. What you are really buying is that every message about your script's command line is written by your script.
- Why must the case branch be written `\?)` rather than `?)`?Inside a case branch the pattern is matched as a shell pattern, where an unescaped `?` matches any single character — so `?)` would swallow every one-letter option, not just the error case. Escaping it as `\?` (or quoting it) matches a literal question mark.
- If getopts reports an invalid option, does the loop stop?No. getopts still returns success, so the loop continues with the next argument; only the end of the arguments, an operand, or `--` ends it. A script with no `\?` branch therefore keeps running on its defaults after a typo, which is why that branch should print usage and exit.
- What does setting OPTERR=0 give you that a leading colon does not, and vice versa?OPTERR=0 only suppresses bash's own message; the variable is still `?` for both failure kinds and OPTARG is still unset, so you cannot tell them apart. The leading colon suppresses the message *and* splits the cases, setting `:` for a missing value and putting the option letter in OPTARG.
saying these in an interview costs you the question
- Thinks the leading colon declares an option named ':'
- Believes the leading colon makes options optional
- Says an invalid option ends the getopts loop
- Assumes OPTARG is set on errors in the default mode
- Writes ?) unescaped in the case block