How to Parse Command-Line Options in Bash with getopts

By 

Updated on

13 min read

Bash 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:

sh
while getopts "optstring" VARNAME; do
    case $VARNAME in
        # handle each option
    esac
done
  • optstring - A string that defines which options the script accepts
  • VARNAME - 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:

~/flags.shsh
#!/bin/bash

while getopts "vh" opt; do
    case $opt in
        v) echo "Verbose mode enabled" ;;
        h) echo "Usage: $0 [-v] [-h]"; exit 0 ;;
    esac
done

Run the script:

Terminal
./flags.sh -v
./flags.sh -h
./flags.sh -vh
output
Verbose 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 -v for 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, getopts suppresses its default error messages so you can handle errors yourself.

For example, the option string ":vf:o:" means:

  • : - Silent error mode
  • v - 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:

~/optarg_example.shsh
#!/bin/bash

while getopts "f:o:" opt; do
    case $opt in
        f) echo "Input file: $OPTARG" ;;
        o) echo "Output file: $OPTARG" ;;
    esac
done
Terminal
./optarg_example.sh -f data.csv -o results.txt
output
Input file: data.csv
Output file: results.txt

OPTIND

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 $@:

~/optind_example.shsh
#!/bin/bash

while getopts "v" opt; do
    case $opt in
        v) echo "Verbose mode enabled" ;;
    esac
done

shift $((OPTIND - 1))

echo "Remaining arguments: $@"
Terminal
./optind_example.sh -v file1.txt file2.txt
output
Verbose mode enabled
Remaining arguments: file1.txt file2.txt

The 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:

Terminal
./optind_example.sh -v -- -notafile.txt
output
Verbose mode enabled
Remaining arguments: -notafile.txt

This 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:

~/verbose_errors.shsh
#!/bin/bash

while getopts "f:" opt; do
    case $opt in
        f) echo "File: $OPTARG" ;;
    esac
done
Terminal
./verbose_errors.sh -x
./verbose_errors.sh -f
output
./verbose_errors.sh: illegal option -- x
./verbose_errors.sh: option requires an argument -- f

In 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, opt is set to ? and OPTARG contains the invalid option character.
  • For a missing argument, opt is set to : and OPTARG contains the option that was missing its argument.
~/silent_errors.shsh
#!/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
Terminal
./silent_errors.sh -x
./silent_errors.sh -f
output
Error: Invalid option -x
Error: Option -f requires an argument

Silent 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.

~/process.shsh
#!/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
done
Terminal
echo -e "line 1\nline 2\nline 3\nline 4\nline 5" > testfile.txt
./process.sh -v -n 3 testfile.txt
output
Verbose: ON
Output: stdout
Count: 3
Files: testfile.txt

line 1
line 2
line 3

The 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:

~/deploy.shsh
#!/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
Terminal
./deploy.sh -e production -t v2.1.0 -d webapp
output
Deploying '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 production

getopts vs getopt

The getopts built-in is often confused with the external getopt command. Here are the key differences:

Featuregetopts (built-in)getopt (external)
TypeBash/POSIX built-inExternal program (/usr/bin/getopt)
Long optionsNot supportedSupported (--verbose)
PortabilityWorks on all POSIX shellsVaries by OS (GNU vs BSD)
Whitespace handlingHandles correctlyGNU version handles correctly
Error handlingBuilt-in verbose/silent modesReports diagnostics; -q suppresses them
SpeedFaster (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:

~/backup.shsh
#!/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:

Terminal
./backup.sh --verbose --output=backup.tar /home/data
./backup.sh -v -o backup.tar /home/data
output
Verbose: true
Output: backup.tar
Source: /home/data
Verbose: true
Output: backup.tar
Source: /home/data

The 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:

~/backup_getopt.shsh
#!/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 .

ElementDescription
getopts "opts" varParse options defined in opts, store current letter in var
f in option stringSimple flag, no argument
f: in option stringOption that requires an argument
: (leading)Enable silent error mode
$OPTARGHolds the argument for the current option
$OPTINDIndex of the next argument to process
shift $((OPTIND - 1))Remove parsed options, keep remaining arguments
\? in caseHandles invalid options
: in caseHandles 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:

~/optind_local.shsh
#!/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 second
output
Name: first
Name: second

Remove 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

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