0Pricing
DevOps Bootcamp · Lesson

Parsing Flags and Arguments with getopts

Implement professional command-line interfaces using getopts for short options, required arguments, and usage messages.

Parsing Flags and Arguments with getopts is a free DevOps Bootcamp lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why getopts Exists

Every real-world script eventually needs to accept options like -v, -o output.txt, or -n 5. Parsing these by hand with $1, $2… becomes brittle fast.

getopts is the POSIX-standard built-in that handles short options (-a, -b) reliably, including options that take an argument. It is built into every POSIX shell, so no dependencies are required.

  • Handles combined flags: -vn 5 = -v -n 5
  • Reports unknown options gracefully
  • Sets standard variables OPTIND and OPTARG automatically

In this lesson you will build a complete, professional CLI using getopts from scratch.

The getopts Syntax

The core syntax is a while loop that calls getopts on every iteration:

while getopts "optstring" varname; do
  case "$varname" in
    ...
  esac
done
  • optstring — a string listing each accepted option letter. A colon after a letter means that option requires an argument.
  • varname — receives the current option letter on each iteration.
  • OPTARG — automatically set to the argument value when a colon follows the letter.
  • OPTIND — index of the next argument to process; shift with shift $((OPTIND - 1)) after the loop to expose remaining positional parameters.
#!/usr/bin/env bash
# Minimal skeleton — shows the loop structure
while getopts 'vn:' opt; do
  case "$opt" in
    v) echo "Verbose mode on" ;;
    n) echo "Count = $OPTARG" ;;
    ?) echo "Unknown option: -$OPTARG" >&2; exit 1 ;;
  esac
done

Defining an Optstring

The optstring is a compact declaration of your CLI contract. Each character represents one accepted flag.

  • 'abc' — accepts -a, -b, -c (no arguments)
  • 'a:bc'-a requires an argument; -b and -c do not
  • ':abc' — leading colon enables silent error mode (your script handles bad options instead of the shell printing a message)

Silent mode is preferred in production scripts because it gives you full control over error messages and exit codes.

#!/usr/bin/env bash
# optstring ':o:vq'
# -o  requires an argument (output file)
# -v  verbose flag (no argument)
# -q  quiet flag  (no argument)
# Leading ':' = silent error mode

while getopts ':o:vq' opt; do
  case "$opt" in
    o) OUTPUT="$OPTARG" ;;
    v) VERBOSE=1 ;;
    q) QUIET=1 ;;
    :) echo "Error: -$OPTARG requires an argument" >&2; exit 1 ;;
    \?) echo "Error: unknown option -$OPTARG" >&2; exit 1 ;;
  esac
done
echo "OUTPUT=$OUTPUT  VERBOSE=$VERBOSE  QUIET=$QUIET"

OPTARG and Required Arguments

When a flag is followed by a colon in the optstring, getopts stores its value in OPTARG. The user can write the argument either with or without a space:

  • -o report.txt
  • -oreport.txt

Both forms are parsed identically. This is one of the key advantages over manual $1/shift parsing.

In silent mode (':' leading the optstring), a missing argument causes getopts to set varname to : and OPTARG to the option letter — perfect for a targeted error message.

#!/usr/bin/env bash
# Demonstrate OPTARG with a file-processing script

while getopts ':i:o:' opt; do
  case "$opt" in
    i) INPUT="$OPTARG" ;;
    o) OUTPUT="$OPTARG" ;;
    :) echo "Error: option -$OPTARG needs a value" >&2; exit 1 ;;
    \?) echo "Error: unknown flag -$OPTARG" >&2; exit 1 ;;
  esac
done

echo "Input  : ${INPUT:-<not set>}"
echo "Output : ${OUTPUT:-<not set>}"

Shifting Past Options with OPTIND

After getopts finishes, OPTIND holds the index of the first non-option argument. Use shift to remove all processed options so that $1, $2… refer to the remaining positional parameters (e.g., filenames).

The idiom is always:

shift $((OPTIND - 1))

After the shift, $@ contains only the arguments that were not flags — the operands of your command.

#!/usr/bin/env bash
# Shows OPTIND shift and leftover positional args

VERBOSE=0
while getopts ':vn:' opt; do
  case "$opt" in
    v) VERBOSE=1 ;;
    n) COUNT="$OPTARG" ;;
    :) echo "Error: -$OPTARG requires an argument" >&2; exit 1 ;;
    \?) echo "Error: unknown option -$OPTARG" >&2; exit 1 ;;
  esac
done
shift $((OPTIND - 1))

echo "VERBOSE=$VERBOSE  COUNT=${COUNT:-1}"
echo "Remaining args: $*"

Writing a Usage Function

A professional script always provides a usage() function that prints a help message and exits. Convention is:

  • Print to stderr (fd 2) so it does not pollute piped output
  • Exit with code 0 for -h / --help, exit with code 1 for invalid usage
  • Call usage 1 from error paths and usage 0 from the -h handler
#!/usr/bin/env bash

usage() {
  cat >&2 <<EOF
Usage: $(basename "$0") [-v] [-n COUNT] [-o FILE] [FILE...]

Options:
  -v          Verbose output
  -n COUNT    Repeat COUNT times (default: 1)
  -o FILE     Write output to FILE
  -h          Show this help
EOF
  exit "${1:-0}"
}

while getopts ':vn:o:h' opt; do
  case "$opt" in
    v) VERBOSE=1 ;;
    n) COUNT="$OPTARG" ;;
    o) OUTFILE="$OPTARG" ;;
    h) usage 0 ;;
    :) echo "Error: -$OPTARG requires a value" >&2; usage 1 ;;
    \?) echo "Error: unknown option -$OPTARG" >&2; usage 1 ;;
  esac
done
shift $((OPTIND - 1))
echo "Parsed OK — verbose=${VERBOSE:-0} count=${COUNT:-1} out=${OUTFILE:--}"

Default Values and Validation

After parsing, validate and set defaults before doing any real work. Keep the parsing phase (the loop) separate from the logic phase. This makes both sections easier to read and test.

  • Use ${VAR:-default} for inline defaults
  • Validate numeric arguments with a regex or arithmetic check
  • Validate that required options were actually provided
#!/usr/bin/env bash

usage() { echo "Usage: $(basename "$0") -n COUNT [-v]" >&2; exit 1; }

VERBOSE=0
COUNT=''

while getopts ':n:v' opt; do
  case "$opt" in
    n) COUNT="$OPTARG" ;;
    v) VERBOSE=1 ;;
    :) echo "Error: -$OPTARG needs a value" >&2; usage ;;
    \?) echo "Error: -$OPTARG unknown" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))

# Validation phase
[[ -z "$COUNT" ]] && { echo "Error: -n COUNT is required" >&2; usage; }
[[ "$COUNT" =~ ^[0-9]+$ ]] || { echo "Error: COUNT must be a positive integer" >&2; usage; }

for (( i=1; i<=COUNT; i++ )); do
  [[ $VERBOSE -eq 1 ]] && echo "Iteration $i of $COUNT"
  echo "Hello, world!"
done

Combining Flags on the Command Line

getopts automatically handles combined short flags written without spaces, which is the standard Unix convention:

  • -v -q is equivalent to -vq
  • -n 5 -v is equivalent to -n5 -v or -vn5

You do not need to write any extra code to support this — getopts iterates through each character of a combined option string automatically. This is another major reason to use getopts instead of manual parsing.

#!/usr/bin/env bash
# Test combined flag parsing
# Run as:  bash script.sh -vq -n3

VERBOSE=0; QUIET=0; COUNT=1

while getopts ':vqn:' opt; do
  case "$opt" in
    v) VERBOSE=1 ;;
    q) QUIET=1 ;;
    n) COUNT="$OPTARG" ;;
    :) echo "Error: -$OPTARG needs value" >&2; exit 1 ;;
    \?) echo "Error: unknown -$OPTARG" >&2; exit 1 ;;
  esac
done
shift $((OPTIND - 1))

echo "verbose=$VERBOSE quiet=$QUIET count=$COUNT"

Handling the Double-Dash Separator

Unix commands accept -- (double-dash) as an explicit signal that option processing should stop. Everything after -- is treated as a positional argument, even if it looks like a flag.

getopts stops automatically when it encounters --. After shift $((OPTIND - 1)), the double-dash is gone and $@ contains only the operands.

This is important for scripts that operate on filenames that could begin with a dash, for example:

myscript.sh -v -- -strangefile.txt
#!/usr/bin/env bash
# Demonstrate -- separator
# Run as:  bash script.sh -v -- file1.txt -oddname.txt

VERBOSE=0
while getopts ':v' opt; do
  case "$opt" in
    v) VERBOSE=1 ;;
    \?) echo "Unknown option -$OPTARG" >&2; exit 1 ;;
  esac
done
shift $((OPTIND - 1))   # removes -v and the '--' separator

echo "Verbose: $VERBOSE"
echo "Files to process:"
for f in "$@"; do
  echo "  -> $f"
done

Wrapping getopts in a Library Function

In modular scripts, you can encapsulate getopts inside a parse_args() function that sets global (or nameref) variables. This keeps main() clean and allows you to source the parser from other scripts.

Key rules for this pattern:

  • Declare option variables before calling the function
  • Use global variables or pass values via nameref (declare -n)
  • Return a non-zero exit code on bad input so main can react
#!/usr/bin/env bash

# Global option variables
VERBOSE=0; OUTPUT=''; COUNT=1

parse_args() {
  local opt
  while getopts ':vn:o:h' opt; do
    case "$opt" in
      v) VERBOSE=1 ;;
      n) COUNT="$OPTARG" ;;
      o) OUTPUT="$OPTARG" ;;
      h) echo "Usage: $(basename "$0") [-v] [-n N] [-o FILE]"; exit 0 ;;
      :) echo "Error: -$OPTARG needs a value" >&2; return 1 ;;
      \?) echo "Error: unknown option -$OPTARG" >&2; return 1 ;;
    esac
  done
  shift $((OPTIND - 1))
  ARGS=("$@")   # leftover positional args stored in array
}

main() {
  parse_args "$@" || exit 1
  echo "verbose=$VERBOSE count=$COUNT output=${OUTPUT:--} args=${ARGS[*]}"
}

main "$@"

Full Real-World Example: A Log Archiver

Here is a complete, real-world script that uses everything covered in this lesson: optstring with required arguments, silent error mode, a usage function, default values, validation, and the OPTIND shift.

Study the structure — it is the template you should follow in every script you write that needs a CLI interface.

#!/usr/bin/env bash
# archive_logs.sh — compress and move logs older than N days

set -euo pipefail

DESTDIR='/tmp/log_archive'
DAYS=30
VERBOSE=0

usage() {
  cat >&2 <<EOF
Usage: $(basename "$0") [-v] [-d DAYS] [-o DIR] SOURCE_DIR

  -d DAYS   Archive logs older than DAYS (default: 30)
  -o DIR    Destination directory (default: /tmp/log_archive)
  -v        Verbose output
  -h        Show this help
EOF
  exit "${1:-0}"
}

while getopts ':d:o:vh' opt; do
  case "$opt" in
    d) DAYS="$OPTARG" ;;
    o) DESTDIR="$OPTARG" ;;
    v) VERBOSE=1 ;;
    h) usage 0 ;;
    :) echo "Error: -$OPTARG requires a value" >&2; usage 1 ;;
    \?) echo "Error: unknown option -$OPTARG" >&2; usage 1 ;;
  esac
done
shift $((OPTIND - 1))

# Validation
[[ $# -lt 1 ]] && { echo "Error: SOURCE_DIR is required" >&2; usage 1; }
[[ "$DAYS" =~ ^[0-9]+$ ]] || { echo "Error: DAYS must be numeric" >&2; exit 1; }
SOURCE="$1"
[[ -d "$SOURCE" ]] || { echo "Error: '$SOURCE' is not a directory" >&2; exit 1; }

mkdir -p "$DESTDIR"
[[ $VERBOSE -eq 1 ]] && echo "Archiving logs older than $DAYS days from $SOURCE to $DESTDIR"

find "$SOURCE" -name '*.log' -mtime "+$DAYS" -print | while read -r f; do
  gzip -c "$f" > "$DESTDIR/$(basename "$f").gz"
  [[ $VERBOSE -eq 1 ]] && echo "  archived: $f"
done

echo "Done."

Quick Check: getopts Optstring

Read the following getopts call and choose the correct description of its behavior:

while getopts ':f:vq' opt; do

Lesson Recap: getopts Mastery

You have learned how to build professional command-line interfaces in Bash using getopts. Here is a summary of the key points:

  • Optstring syntax — letters without a colon are boolean flags; a colon after a letter means it requires an argument; a leading colon enables silent error mode.
  • OPTARG — automatically holds the argument value for options that require one.
  • OPTIND — use shift $((OPTIND - 1)) after the loop to expose remaining positional parameters in $@.
  • Silent error mode — preferred in production; handle : (missing argument) and \? (unknown option) cases yourself for full control.
  • usage() function — always write one; print to stderr, exit 0 for -h, exit 1 for errors.
  • Combined flags — getopts handles -vq and -n5 automatically with no extra code.
  • Modular pattern — wrap getopts in a parse_args() function for clean, reusable scripts.

Mastering getopts transforms your scripts from single-purpose tools into reliable, user-friendly CLI programs that follow Unix conventions.

Frequently asked questions

Is the “Parsing Flags and Arguments with getopts” lesson free?

Yes — the full text of “Parsing Flags and Arguments with getopts” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.

What will I learn in “Parsing Flags and Arguments with getopts”?

Implement professional command-line interfaces using getopts for short options, required arguments, and usage messages. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start DevOps Bootcamp?

No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Parsing Flags and Arguments with getopts” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this DevOps Bootcamp lesson?

Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Designing Functions with Local Scope and Return Codes
  2. Building and Sourcing Reusable Bash Libraries
  3. Parsing Flags and Arguments with getopts
  4. Passing Arrays and Associative Maps Between Functions
← Back to DevOps Bootcamp