How to Parse Command-Line Options in Bash with getopts

Positional arguments work well for small Bash scripts, but they become difficult to manage when a script needs flags such as -v or named inputs such as -o output. The Bash getopts built-in parses these short options, extracts their arguments, and reports invalid input without manually stepping through $@ with shift.
This guide explains how to use getopts to process options and arguments in Bash scripts. If you are new to command-line arguments, start with our guide on positional parameters
.
Syntax
The basic syntax for getopts is:
while getopts "optstring" VARNAME; do
case $VARNAME in
# handle each option
esac
doneoptstring- A string that defines which options the script acceptsVARNAME- A variable that holds the current option letter on each iteration
Each time the while loop runs, getopts processes the next option from the command line and stores the option letter in VARNAME. The loop ends when there are no more options to process.
Here is a simple example:
#!/bin/bash
while getopts "vh" opt; do
case $opt in
v) echo "Verbose mode enabled" ;;
h) echo "Usage: $0 [-v] [-h]"; exit 0 ;;
esac
doneRun the script:
./flags.sh -v
./flags.sh -h
./flags.sh -vhVerbose mode enabled
Usage: ./flags.sh [-v] [-h]
Verbose mode enabled
Usage: ./flags.sh [-v] [-h]Notice that -vh works the same as -v -h. The getopts command automatically handles combined short options.
The Option String
The option string tells getopts which option letters are valid and which ones require an argument. There are three patterns:
f- A simple flag (no argument). Used for boolean switches like-vfor verbose.f:- An option that requires an argument. The colon after the letter means the user must provide a value, such as-f filename.:(leading colon) - Enables silent error mode. When placed at the very beginning of the option string,getoptssuppresses its default error messages so you can handle errors yourself.
For example, the option string ":vf:o:" means:
:- Silent error modev- A simple flag (-v)f:- An option requiring an argument (-f filename)o:- An option requiring an argument (-o output)
OPTARG and OPTIND
When working with getopts, two special variables track the parsing state:
OPTARG
The OPTARG variable holds the argument value for options that require one. When you define an option with a trailing colon (e.g., f:), the value the user passes after -f is stored in OPTARG:
#!/bin/bash
while getopts "f:o:" opt; do
case $opt in
f) echo "Input file: $OPTARG" ;;
o) echo "Output file: $OPTARG" ;;
esac
done./optarg_example.sh -f data.csv -o results.txtInput file: data.csv
Output file: results.txtOPTIND
The OPTIND variable holds the index of the next argument to be processed. It starts at 1 and increments as getopts processes each option. After the while loop finishes, OPTIND points to the first non-option argument.
Use shift $((OPTIND - 1)) after the loop to remove all processed options, leaving only the remaining positional arguments in $@:
#!/bin/bash
while getopts "v" opt; do
case $opt in
v) echo "Verbose mode enabled" ;;
esac
done
shift $((OPTIND - 1))
echo "Remaining arguments: $@"./optind_example.sh -v file1.txt file2.txtVerbose mode enabled
Remaining arguments: file1.txt file2.txtThe shift $((OPTIND - 1)) line is a common pattern. Without it, the processed options would still be part of the positional parameters, making it difficult to access the non-option arguments.
A bare -- marks the end of the options. When getopts reaches it, parsing stops and OPTIND points to the argument that follows, so everything after -- is treated as a positional argument even when it starts with a dash:
./optind_example.sh -v -- -notafile.txtVerbose mode enabled
Remaining arguments: -notafile.txtThis is how you pass a filename that begins with a dash without getopts mistaking it for an option.
Error Handling
The getopts command has two error handling modes: verbose (default) and silent.
Verbose Mode (Default)
In verbose mode, getopts prints its own error messages when it encounters an invalid option or a missing argument:
#!/bin/bash
while getopts "f:" opt; do
case $opt in
f) echo "File: $OPTARG" ;;
esac
done./verbose_errors.sh -x
./verbose_errors.sh -f./verbose_errors.sh: illegal option -- x
./verbose_errors.sh: option requires an argument -- fIn this mode, getopts sets opt to ? for both invalid options and missing arguments.
Silent Mode
Silent mode is enabled by adding a colon at the beginning of the option string. In this mode, getopts suppresses its default error messages and gives you more control:
- For an invalid option,
optis set to?andOPTARGcontains the invalid option character. - For a missing argument,
optis set to:andOPTARGcontains the option that was missing its argument.
#!/bin/bash
while getopts ":f:vh" opt; do
case $opt in
f) echo "File: $OPTARG" ;;
v) echo "Verbose mode" ;;
h) echo "Usage: $0 [-v] [-f file] [-h]"; exit 0 ;;
\?) echo "Error: Invalid option -$OPTARG" >&2; exit 1 ;;
:) echo "Error: Option -$OPTARG requires an argument" >&2; exit 1 ;;
esac
done./silent_errors.sh -x
./silent_errors.sh -fError: Invalid option -x
Error: Option -f requires an argumentSilent mode is the recommended approach for production scripts because it allows you to write custom error messages that are more helpful to the user.
Practical Examples
Example 1: Script with Flags and Arguments
This script demonstrates a common pattern: a usage function , boolean flags, options with arguments, and input validation.
#!/bin/bash
usage() {
echo "Usage: $0 [-v] [-o output] [-n count] file..."
echo ""
echo "Options:"
echo " -v Enable verbose output"
echo " -o output Write results to output file"
echo " -n count Number of lines to process"
echo " -h Show this help message"
exit 1
}
VERBOSE=false
OUTPUT=""
COUNT=0
while getopts ":vo:n:h" opt; do
case $opt in
v) VERBOSE=true ;;
o) OUTPUT="$OPTARG" ;;
n)
case $OPTARG in
''|*[!0-9]*|0)
echo "Error: Count must be a positive integer" >&2
usage
;;
*) COUNT="$OPTARG" ;;
esac
;;
h) usage ;;
\?) echo "Error: Invalid option -$OPTARG" >&2; usage ;;
:) echo "Error: Option -$OPTARG requires an argument" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
if [ $# -eq 0 ]; then
echo "Error: No input files specified" >&2
usage
fi
if [ "$VERBOSE" = true ]; then
echo "Verbose: ON"
echo "Output: ${OUTPUT:-stdout}"
if [ "$COUNT" -gt 0 ]; then
echo "Count: $COUNT"
else
echo "Count: all"
fi
echo "Files: $@"
echo ""
fi
for file in "$@"; do
if [ ! -f "$file" ]; then
echo "Warning: '$file' not found, skipping" >&2
continue
fi
if [ -n "$OUTPUT" ]; then
if [ "$COUNT" -gt 0 ]; then
head -n "$COUNT" "$file" >> "$OUTPUT"
else
cat "$file" >> "$OUTPUT"
fi
else
if [ "$COUNT" -gt 0 ]; then
head -n "$COUNT" "$file"
else
cat "$file"
fi
fi
doneecho -e "line 1\nline 2\nline 3\nline 4\nline 5" > testfile.txt
./process.sh -v -n 3 testfile.txtVerbose: ON
Output: stdout
Count: 3
Files: testfile.txt
line 1
line 2
line 3The script parses the options first, then uses shift to access the remaining file arguments.
Example 2: Configuration Wrapper
This example shows a script that wraps another command, passing different configurations based on the options provided:
#!/bin/bash
ENV="staging"
DRY_RUN=false
TAG="latest"
while getopts ":e:t:dh" opt; do
case $opt in
e) ENV="$OPTARG" ;;
t) TAG="$OPTARG" ;;
d) DRY_RUN=true ;;
h)
echo "Usage: $0 [-e environment] [-t tag] [-d] service"
echo ""
echo " -e env Target environment (default: staging)"
echo " -t tag Image tag (default: latest)"
echo " -d Dry run mode"
exit 0
;;
\?) echo "Error: Invalid option -$OPTARG" >&2; exit 1 ;;
:) echo "Error: Option -$OPTARG requires an argument" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
SERVICE="${1:?Error: Service name required}"
echo "Deploying '$SERVICE' to $ENV with tag '$TAG'"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would execute: docker pull myregistry/$SERVICE:$TAG"
echo "[DRY RUN] Would execute: kubectl set image deployment/$SERVICE $SERVICE=myregistry/$SERVICE:$TAG -n $ENV"
else
echo "Pulling image and updating deployment..."
fi./deploy.sh -e production -t v2.1.0 -d webappDeploying 'webapp' to production with tag 'v2.1.0'
[DRY RUN] Would execute: docker pull myregistry/webapp:v2.1.0
[DRY RUN] Would execute: kubectl set image deployment/webapp webapp=myregistry/webapp:v2.1.0 -n productiongetopts vs getopt
The getopts built-in is often confused with the external getopt command. Here are the key differences:
| Feature | getopts (built-in) | getopt (external) |
|---|---|---|
| Type | Bash/POSIX built-in | External program (/usr/bin/getopt) |
| Long options | Not supported | Supported (--verbose) |
| Portability | Works on all POSIX shells | Varies by OS (GNU vs BSD) |
| Whitespace handling | Handles correctly | GNU version handles correctly |
| Error handling | Built-in verbose/silent modes | Reports diagnostics; -q suppresses them |
| Speed | Faster (no subprocess) | Slower (spawns a process) |
Use getopts when you only need short options and want maximum portability. Use getopt (GNU version) when you need long options like --verbose or --output.
Handling Long Options
Since getopts cannot parse --verbose or --output=file, scripts that need long options have to handle them another way. The portable approach is a manual while loop over the positional parameters, which works on any POSIX shell and gives you full control over the syntax you accept:
#!/bin/bash
VERBOSE=false
OUTPUT=""
while [ $# -gt 0 ]; do
case $1 in
-v|--verbose) VERBOSE=true; shift ;;
-o|--output)
if [ $# -lt 2 ]; then
echo "Error: $1 requires an argument" >&2
exit 1
fi
OUTPUT="$2"
shift 2
;;
--output=*) OUTPUT="${1#*=}"; shift ;;
-h|--help) echo "Usage: $0 [-v|--verbose] [-o|--output FILE] source"; exit 0 ;;
--) shift; break ;;
-*) echo "Error: Unknown option $1" >&2; exit 1 ;;
*) break ;;
esac
done
if [ $# -ne 1 ]; then
echo "Error: Exactly one source is required" >&2
exit 1
fi
echo "Verbose: $VERBOSE"
echo "Output: ${OUTPUT:-none}"
echo "Source: $1"Each branch consumes what it needs and shifts accordingly: a flag shifts once, an option with a separate argument shifts twice, and the --output=* branch uses parameter expansion
to strip everything up to the = sign. Before shifting twice, the -o|--output branch checks that an argument is available. After the loop, the script verifies that exactly one source was provided. The -* branch catches anything unrecognized, and the * branch breaks out of the loop as soon as a non-option argument appears.
Both spellings produce the same result:
./backup.sh --verbose --output=backup.tar /home/data
./backup.sh -v -o backup.tar /home/dataVerbose: true
Output: backup.tar
Source: /home/data
Verbose: true
Output: backup.tar
Source: /home/dataThe other approach is GNU getopt, which rewrites the argument list into a normalized form that a plain case loop can walk through. It accepts abbreviations such as --verb and splits --output=file for you:
#!/bin/bash
PARSED=$(getopt -o vo:h --long verbose,output:,help -n "$0" -- "$@") || exit 1
eval set -- "$PARSED"
VERBOSE=false
OUTPUT=""
while true; do
case $1 in
-v|--verbose) VERBOSE=true; shift ;;
-o|--output) OUTPUT="$2"; shift 2 ;;
-h|--help) echo "Usage: $0 [-v] [-o FILE] source"; exit 0 ;;
--) shift; break ;;
esac
done
if [ $# -ne 1 ]; then
echo "Error: Exactly one source is required" >&2
exit 1
fi
echo "Verbose: $VERBOSE, Output: ${OUTPUT:-none}, Source: $1"The -o flag lists the short options, --long lists the long ones, and eval set -- "$PARSED" replaces the positional parameters with the normalized list. This only works with the GNU version from util-linux. The BSD getopt shipped on macOS does not support --long, so the manual loop is the safer choice for scripts that need to run everywhere.
Quick Reference
For a printable quick reference, see the Bash cheatsheet .
| Element | Description |
|---|---|
getopts "opts" var | Parse options defined in opts, store current letter in var |
f in option string | Simple flag, no argument |
f: in option string | Option that requires an argument |
: (leading) | Enable silent error mode |
$OPTARG | Holds the argument for the current option |
$OPTIND | Index of the next argument to process |
shift $((OPTIND - 1)) | Remove parsed options, keep remaining arguments |
\? in case | Handles invalid options |
: in case | Handles missing arguments (silent mode only) |
Troubleshooting
Options are not being parsed
Make sure your options come before any non-option arguments. The getopts command stops parsing when it encounters the first non-option argument. For example, ./script.sh file.txt -v will not parse -v because file.txt comes first.
OPTIND is not resetting between function calls
The OPTIND variable is global, so a function
that runs getopts leaves it pointing past the arguments it already consumed. Calling that function a second time parses nothing at all. Declaring OPTIND as local resets it to 1 on every call:
#!/bin/bash
parse() {
local OPTIND opt
while getopts ":n:" opt; do
case $opt in
n) echo "Name: $OPTARG" ;;
esac
done
}
parse -n first
parse -n secondName: first
Name: secondRemove the local OPTIND line and only Name: first is printed, because the second call starts parsing past the end of its own arguments.
Missing argument not detected
If an option requires an argument but getopts does not report an error, check that you included a colon after the option letter in the option string. For example, use "f:" instead of "f" if -f needs an argument.
Unexpected ? in the variable
In verbose mode (no leading colon), getopts sets the variable to ? for both invalid options and missing arguments. Switch to silent mode (leading colon) to distinguish between the two cases and write custom error messages.
FAQ
Does getopts support long options like –verbose?
No. The getopts built-in only supports single-character options (e.g., -v). For long options, use the external GNU getopt command or parse them manually with a case statement
and shift. Both approaches are covered in the section on handling long options above.
Can I combine options like -vf file?
Yes. The getopts command automatically handles combined options. When it encounters -vf file, it processes -v first, then -f with file as its argument.
What happens if I forget shift $((OPTIND - 1))?
The processed options will remain in the positional parameters. Any code that accesses $1, $2, or $@ after the getopts loop will still see the option flags instead of just the remaining arguments.
How do I require a filename but keep the option optional?
Options are already optional, since getopts stops at the first non-option argument. Enforce the operand yourself after shift $((OPTIND - 1)) by testing $#, for example if [ $# -ne 1 ] for a script that takes exactly one file. The option still has to come before the filename.
Is getopts POSIX compliant?
Yes. The getopts command is defined by the POSIX standard and works in all POSIX-compliant shells, including bash, dash, ksh, and zsh. This makes it more portable than the external getopt command.
How do I make an option’s argument optional?
The getopts built-in does not support optional arguments for options. An option either always requires an argument (using :) or never takes one. If you need optional arguments, handle the logic manually after parsing.
Conclusion
The getopts built-in parses short command-line options in Bash scripts, handling option strings, argument extraction, combined flags, and error reporting with very little code. Silent mode with a leading colon costs two extra case branches and is worth it in any script you intend to keep, since it lets you write error messages that tell the user what to fix. For the simpler case of ordered arguments without flags, see our guide on passing arguments to a Bash script
.
Tags
Linuxize Weekly Newsletter
A quick weekly roundup of new tutorials, news, and tips.
About the authors

Dejan Panovski
Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 800+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.
View author page