Skip to content
CloudOps
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 8 min read

Parsing Arguments in Bash Scripts the Right Way

Positional args break the moment someone passes flags out of order. Here's how to parse bash arguments with getopts and a hand-rolled loop that handles long options.

  • #bash
  • #python
  • #getopts
  • #cli
  • #scripting
  • #automation

The fastest way to make a useful script hated by your team is to make its arguments positional and undocumented. ./deploy.sh prod true 3 west — nobody remembers what true means six weeks later, and passing the args out of order silently deploys the wrong thing.

In 25 years of writing operational bash, the scripts that survived were the ones with named flags, a --help, and sane defaults. Here is how I parse arguments so a script stays usable.

Why positional arguments rot

Positional arguments work for two-argument throwaways. They fail the moment a script grows a third option, an optional flag, or a value with a space in it. There is no --help, no validation, and the call site is unreadable.

The fix is named flags. There are two good ways to do it in bash, and one I reach for depending on whether I need long options.

Option 1: getopts for short flags

getopts is built into bash and is the right tool when single-letter flags are enough:

#!/usr/bin/env bash
set -Eeuo pipefail

env="staging"
replicas=1
verbose=0

usage() {
  cat <<'EOF'
Usage: deploy.sh [-e ENV] [-r REPLICAS] [-v]
  -e  target environment (default: staging)
  -r  replica count (default: 1)
  -v  verbose output
EOF
}

while getopts ":e:r:vh" opt; do
  case "$opt" in
    e) env="$OPTARG" ;;
    r) replicas="$OPTARG" ;;
    v) verbose=1 ;;
    h) usage; exit 0 ;;
    :) echo "option -$OPTARG needs a value" >&2; exit 1 ;;
    \?) echo "unknown option: -$OPTARG" >&2; usage; exit 1 ;;
  esac
done
shift $((OPTIND - 1))

A few details that matter:

  • The leading : in ":e:r:vh" enables silent error handling, so you control the messages instead of getopts printing its own.
  • A : after a letter (e:) means that flag takes a value, available in $OPTARG.
  • shift $((OPTIND - 1)) drops the parsed flags, leaving any positional arguments in $@.

The downside: getopts does not do long options like --env. For internal scripts that is fine. For anything other people type, they will want --env.

Option 2: a hand-rolled loop for long options

When I want --env production and --verbose, I parse manually with a while/case loop:

env="staging"
replicas=1
verbose=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    -e|--env)      env="$2"; shift 2 ;;
    -r|--replicas) replicas="$2"; shift 2 ;;
    -v|--verbose)  verbose=1; shift ;;
    -h|--help)     usage; exit 0 ;;
    --env=*)       env="${1#*=}"; shift ;;
    --replicas=*)  replicas="${1#*=}"; shift ;;
    --)            shift; break ;;
    -*)            echo "unknown option: $1" >&2; exit 1 ;;
    *)             args+=("$1"); shift ;;
  esac
done

This handles --env production and --env=production, short aliases, a -- end-of-options marker, and collects leftover positionals into an args array. It is more code than getopts, but it is the interface people expect from a real CLI tool.

Validate after parsing, not during

Resist the urge to validate inside the case block. Parse first, validate second — it keeps the logic clean and the error messages together:

[[ "$env" =~ ^(staging|production)$ ]] || {
  echo "env must be staging or production, got: $env" >&2
  exit 1
}

[[ "$replicas" =~ ^[0-9]+$ ]] || {
  echo "replicas must be a number, got: $replicas" >&2
  exit 1
}

Now a typo’d environment or a non-numeric replica count fails immediately with a clear message, instead of failing weirdly three steps into the deploy.

Always ship a --help

A script without --help is a script people guess at. The usage() function above costs you five minutes and saves every future caller from reading the source. Make -h/--help exit zero so it composes nicely in pipelines and scripts.

When the script gets big, leave bash

There is a line I have learned to respect: if argument parsing is getting longer than the actual work, the tool wants to be Python with click or typer, not bash. Subcommands, nested options, rich help text — bash can do it, but you will fight it. Bash argument parsing is for flat, flag-driven scripts. Anything with tool deploy --to prod subcommand structure belongs in a real CLI framework.

Letting AI scaffold the boilerplate

Argument parsing is pure boilerplate, which makes it ideal to hand to AI. I describe the interface and let it generate the loop:

“Write a bash argument-parsing block for a script called backup.sh. Flags: --source (required path), --dest (required path), --retention (integer days, default 7), --dry-run (boolean). Support both --flag value and --flag=value. Include a usage function and validation for each.”

The output is almost always correct, and reviewing a parsing loop is fast because the pattern is so regular. I keep a couple of these scaffolding prompts in my prompt library.

The short version

  • Two named arguments? Positional is fine.
  • Short flags, internal script? getopts with silent error handling.
  • Long options, tool other people run? Hand-rolled while/case.
  • Subcommands and rich help? Stop — write it in Python.
  • Always: validate after parsing, and always ship --help.

For more patterns like this, including AI prompts for generating safe scripts, see our automation guides.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.