Named options make a shell script easier to automate because callers do not have to remember a fragile positional order. The Bash getopts builtin handles short options one at a time while leaving later file operands available to the script.
Each successful getopts call stores the option letter in a variable, places a required option value in OPTARG, and advances OPTIND to the next argument. A leading colon in the option string enables silent error handling, so the script can print a specific message for an unknown option or a missing value.
The parser accepts -n NAME for a required archive label, -d for dry-run mode, and -h for help. A -- marker ends option parsing explicitly, which allows a remaining file operand such as -old.log to begin with a hyphen.
Related: How to create and run a Bash script
Related: How to use a case statement in Bash
#!/usr/bin/env bash set -u usage() { printf 'Usage: %s -n NAME [-d] [FILE...]\n' "${0##*/}" } name= dry_run=0
The name variable starts empty because -n is required, while dry_run starts at 0 because -d is optional.
while getopts ':n:dh' option; do case $option in n) name=$OPTARG ;; d) dry_run=1 ;; h) usage; exit 0 ;; :) printf 'Missing value for -%s\n' "$OPTARG" >&2; usage >&2; exit 2 ;; \?) printf 'Unknown option: -%s\n' "$OPTARG" >&2; usage >&2; exit 2 ;; esac done
In :n:dh, the first colon enables silent error handling and the colon after n marks -n as requiring a value. The d and h options do not take values.
shift "$((OPTIND - 1))" if [[ -z $name ]]; then printf 'Missing required option: -n\n' >&2 usage >&2 exit 2 fi
OPTIND points to the first unparsed argument. Shifting by one less than that index removes the recognized options and their values while preserving later file operands.
printf 'archive=%s\n' "$name" printf 'dry_run=%s\n' "$dry_run" for file in "$@"; do printf 'input=%s\n' "$file" done
Quoted "$@" expansion passes each remaining operand as one value, including names that contain spaces.
#!/usr/bin/env bash set -u usage() { printf 'Usage: %s -n NAME [-d] [FILE...]\n' "${0##*/}" } name= dry_run=0 while getopts ':n:dh' option; do case $option in n) name=$OPTARG ;; d) dry_run=1 ;; h) usage; exit 0 ;; :) printf 'Missing value for -%s\n' "$OPTARG" >&2; usage >&2; exit 2 ;; \?) printf 'Unknown option: -%s\n' "$OPTARG" >&2; usage >&2; exit 2 ;; esac done shift "$((OPTIND - 1))" if [[ -z $name ]]; then printf 'Missing required option: -n\n' >&2 usage >&2 exit 2 fi printf 'archive=%s\n' "$name" printf 'dry_run=%s\n' "$dry_run" for file in "$@"; do printf 'input=%s\n' "$file" done
$ bash -n archive-plan.sh
No output means Bash parsed the complete file without finding a syntax error.
$ bash archive-plan.sh -n Missing value for -n Usage: archive-plan.sh -n NAME [-d] [FILE...]
The script exits with status 2, allowing a caller to distinguish invalid usage from a successful parse.
$ bash archive-plan.sh -d -n nightly -- app.log -old.log archive=nightly dry_run=1 input=app.log input=-old.log
The -- marker prevents -old.log from being read as another option. Both input= lines confirm that the shift retained the operands after parsing.