Skip to content
5 min read·Lesson 8 of 10

Arguments and getopts

Positional arguments, flags, and the right way to parse them: getopts for short flags, manual loops for long flags.

Argument handling separates throwaway scripts from real tools. A few patterns cover everything you need.

Positional Arguments

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

src="$1"
dest="$2"
echo "from $src to $dest"

Use defaults to make optional positionals safe under set -u:

src="${1:-}"
dest="${2:-/tmp}"

# or fail with a message
src="${1:?usage: $0 SRC [DEST]}"

$@ vs $*

myfn() {
  for a in "$@"; do echo "[$a]"; done
}

myfn "one two" three
# [one two]
# [three]    ← spaces preserved, args separate

# vs $*:
for a in "$*"; do echo "[$a]"; done
# [one two three]   ← all collapsed

Always use "$@" (with quotes) to pass arguments through unchanged. $* is rarely what you want.

shift

shift drops $1, renumbers the rest. Useful for consuming options:

cmd="$1"; shift          # remaining args belong to cmd

case "$cmd" in
  build)  do_build "$@" ;;
  deploy) do_deploy "$@" ;;
  *)      echo "unknown: $cmd"; exit 2 ;;
esac

getopts (Short Flags)

getopts is a Bash builtin for POSIX-style single-letter flags:

verbose=false
file=""
count=1

while getopts ":vf:c:h" opt; do
  case "$opt" in
    v) verbose=true ;;
    f) file="$OPTARG" ;;
    c) count="$OPTARG" ;;
    h) usage; exit 0 ;;
    \?) echo "Invalid option: -$OPTARG" >&2; usage; exit 2 ;;
    :)  echo "Option -$OPTARG requires an argument" >&2; exit 2 ;;
  esac
done
shift $((OPTIND - 1))

echo "verbose=$verbose file=$file count=$count remaining=$*"

Notes:

  • The optstring ":vf:c:h" means: leading : activates "silent error reporting"; v and h take no argument; f: and c: take an argument.
  • OPTARG holds the value; OPTIND is the next argument index.
  • After the loop, shift $((OPTIND - 1)) drops parsed flags so $@ is the positional remainder.

Long Options (--verbose)

getopts doesn't support long options. Write a small loop:

verbose=false
file=""
count=1
positional=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    -v|--verbose)
      verbose=true
      shift
      ;;
    -f|--file)
      file="$2"
      shift 2
      ;;
    --file=*)
      file="${1#*=}"
      shift
      ;;
    -c|--count)
      count="$2"
      shift 2
      ;;
    -h|--help)
      usage; exit 0
      ;;
    --)
      shift
      positional+=("$@")
      break
      ;;
    -*)
      echo "Unknown option: $1" >&2
      exit 2
      ;;
    *)
      positional+=("$1")
      shift
      ;;
  esac
done

set -- "${positional[@]}"
echo "remaining: $*"

This handles --file foo, --file=foo, -f foo, mixing flags with positional arguments, and the conventional -- end-of-options marker.

A Usage Function

usage() {
  cat <<EOF
Usage: $0 [options] SRC DEST

Options:
  -v, --verbose     Verbose output
  -f, --file FILE   Use FILE as input
  -c, --count N     Repeat N times (default 1)
  -h, --help        Show this help
EOF
}

Always print to stderr when usage results from an error; to stdout when from --help.

Subcommands (git-style)

cmd="${1:-}"
[[ -n "$cmd" ]] || { usage; exit 2; }
shift

case "$cmd" in
  build)   build "$@" ;;
  deploy)  deploy "$@" ;;
  status)  status "$@" ;;
  help|-h|--help) usage ;;
  *)       echo "unknown command: $cmd" >&2; exit 2 ;;
esac

Each subcommand can have its own argument parsing — same patterns recursively.

Reading from stdin

if [[ ! -t 0 ]]; then
  # stdin is a pipe, read it
  while IFS= read -r line; do
    process "$line"
  done
else
  # stdin is a terminal — use args instead
  for arg in "$@"; do
    process "$arg"
  done
fi

Tools that work both as filters and with arguments are extra useful.

Environment Variables for Config

For deployment-like scripts, environment variables often beat flags:

: "${LOG_LEVEL:=info}"      # default if unset
: "${RETRIES:=3}"
: "${TIMEOUT:=30}"
: "${API_TOKEN:?must be set}"

echo "log=$LOG_LEVEL retries=$RETRIES"

: "${VAR:=default}" sets a default; : "${VAR:?msg}" errors if unset. Both are no-ops besides their side effect on the variable.

Cert Mapping

CertScope
RHCSA / LFCSWriting parameterised admin scripts
AWS / AzureBuild / deploy CLI helpers

The next lesson covers running scripts on a schedule with cron and systemd timers.

Key Takeaways

  • Always use "$@" (with quotes) to pass arguments through unchanged.
  • getopts handles short single-letter flags well.
  • For long flags (--verbose), write a small case loop.
  • shift consumes positional arguments after parsing.
  • Provide a usage message and a --help flag from day one.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →