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
OPTINDandOPTARGautomatically
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
doneDefining 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'—-arequires an argument;-band-cdo 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 1from error paths andusage 0from the-hhandler
#!/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!"
doneCombining Flags on the Command Line
getopts automatically handles combined short flags written without spaces, which is the standard Unix convention:
-v -qis equivalent to-vq-n 5 -vis equivalent to-n5 -vor-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"
doneWrapping 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
globalvariables or pass values via nameref (declare -n) - Return a non-zero exit code on bad input so
maincan 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; doLesson 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
-vqand-n5automatically 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
- Designing Functions with Local Scope and Return Codes
- Building and Sourcing Reusable Bash Libraries
- Parsing Flags and Arguments with getopts
- Passing Arrays and Associative Maps Between Functions