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.