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.

Steps to parse command line options in Bash with getopts:

  1. Create archive-plan.sh with its interpreter line, usage function, and default values.
    archive-plan.sh
    #!/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.

  2. Add the getopts loop after the default values.
    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.

  3. Add the operand shift and required-name check below the option loop.
    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.

  4. Append the parsed-value output after the required-name check.
    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.

  5. Compare archive-plan.sh with the consolidated file.
    archive-plan.sh
    #!/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
  6. Check archive-plan.sh for Bash syntax errors.
    $ bash -n archive-plan.sh

    No output means Bash parsed the complete file without finding a syntax error.

  7. Confirm that -n without a value reaches the missing-value branch.
    $ 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.

  8. Run the parser with both supported options and two file operands.
    $ 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.