What you will learn
Give a script real command-line flags — short options, an optional long-option layer, a usage message, and validation — instead of positional $1/$2 parsing that breaks the moment argument order changes.
Before you begin
getopts is a Bash builtin and handles short options (-v, -f file) natively. Long options (--verbose) need a small amount of extra plumbing shown below; reach for a heavier framework only if you need far more than this.
1. Parse short options with getopts
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
cat <<EOF
Usage: $(basename "$0") -f FILE [-n COUNT] [-v] [-h]
-f FILE input file (required)
-n COUNT number of records to process (default: 10)
-v verbose output
-h show this help
EOF
}
VERBOSE=0
COUNT=10
FILE=""
while getopts ":f:n:vh" opt; do
case "$opt" in
f) FILE="$OPTARG" ;;
n) COUNT="$OPTARG" ;;
v) VERBOSE=1 ;;
h) usage; exit 0 ;;
\?) echo "unknown option: -$OPTARG" >&2; usage; exit 2 ;;
:) echo "option -$OPTARG requires an argument" >&2; usage; exit 2 ;;
esac
done
shift $((OPTIND - 1))
The leading : in ":f:n:vh" switches getopts into silent error mode, so your \? and : cases handle bad input with a clear message — without it, getopts prints its own terser error and you lose control of the message.
2. Validate after parsing, not during
[[ -n "$FILE" ]] || { echo "error: -f FILE is required" >&2; usage; exit 2; }
[[ -f "$FILE" ]] || { echo "error: file not found: $FILE" >&2; exit 1; }
[[ "$COUNT" =~ ^[0-9]+$ ]] || { echo "error: -n must be a positive integer, got '$COUNT'" >&2; exit 2; }
(( VERBOSE )) && echo "processing ${COUNT} records from ${FILE}"
Separating parsing from validation keeps the case statement focused purely on "which flag is this," and makes it trivial to add a new required-flag check later without touching the parsing loop.
3. Handle remaining positional arguments after flags
# After `shift $((OPTIND - 1))`, "$@" holds whatever wasn't consumed as an option.
if (( $# > 0 )); then
echo "extra positional arguments ignored: $*" >&2
fi
Example output
$ ./process.sh -f data.csv -n 50 -v
processing 50 records from data.csv
$ ./process.sh -n abc -f data.csv
error: -n must be a positive integer, got 'abc'
$ ./process.sh
error: -f FILE is required
Usage: process.sh -f FILE [-n COUNT] [-v] [-h]
...
Verify the result
Run with a missing required flag, an invalid value, an unknown flag, and -h, and confirm each produces the intended message and exit code (0 for help, 2 for usage errors, 1 for a valid-but-wrong input like a missing file).
Troubleshooting
If $OPTARG is empty inside the : case, that's expected — it holds the name of the missing option, not a value, by design.
Next steps
Reuse this parsing skeleton as the front end for ssh-fleet-command-executor or staged-pipeline-runner so both accept consistent, validated flags.