getopts: Parsing Command-Line Options in Bash

getopts: Parsing Command-Line Options in Bash

What You'll Learn

  • How to safely parse option arguments like -a and -b value in shell scripts
  • The role and syntax of getopts optstring / OPTARG / OPTIND
  • How to handle invalid options and missing arguments yourself (silent mode)
  • When to use getopts (shell builtin) vs getopt (external command)

Quick Summary

  • Prefer getopts over hand-rolled parsing (POSIX, portable)
  • The core loop is while getopts ":a:bc" opt; do ... done
  • Options that take an argument are declared as letter: in the optstring (a:)
  • After the loop, run shift $((OPTIND - 1)) to reach the remaining positional arguments

Assumptions (target environment)

  • bash / POSIX sh (getopts is a builtin)
  • Only short options (-a, -v) are supported. Long options like --verbose are not.

What is getopts?

Conclusion: getopts is a shell builtin for option parsing; paired with a while loop it pulls -a / -b value arguments off one at a time.

getopts is a shell builtin that parses command-line options (short - arguments) one at a time. Instead of tearing apart $1 and $2 with a case statement by hand, you get a standardized way to process options.

The basic syntax is:

getopts optstring name [args]
  • optstring: the set of valid option letters (e.g. "abc" means -a -b -c)
  • name: the variable that receives the detected option letter
  • Each call processes one option, returning exit status 0 (true) while options remain

That "returns true while options remain" behavior is exactly what a while loop needs.

Why use getopts instead of hand parsing?

Conclusion: getopts handles bundled options (-abc) and the -- terminator per the standard, which hand-written case statements tend to miss.

Roll your own while [ $# -gt 0 ] with case "$1" in and you end up reimplementing every subtle rule yourself:

  • Bundled options: letting -a -b -c be written as -abc
  • Options with arguments: accepting both -b value and -bvalue
  • The -- terminator: stop treating everything after it as options
  • Detecting invalid options and missing arguments

getopts handles all of this with POSIX-standard behavior. It is portable and avoids bash-only syntax, so it works unchanged in a #!/bin/sh script.

If long options (--output file) are not a hard requirement, reach for getopts first. The code is shorter and the behavior is standardized.

How do you use getopts?

Conclusion: Declare accepted letters in optstring, append : to letters that take an argument; OPTARG holds the value and OPTIND the next index.

Writing the optstring

The optstring is a string of accepted option letters. Append : to a letter that takes an argument.

optstring Meaning
"abc" -a -b -c (no arguments)
"a:bc" -a takes an argument; -b -c do not
":a:bc" Leading : enables silent mode (below)

Basic template

#!/bin/bash

verbose=0
output=""

while getopts "vo:" opt; do
  case "$opt" in
    v) verbose=1 ;;
    o) output="$OPTARG" ;;
    \?) echo "Unknown option: -$OPTARG" >&2; exit 1 ;;
  esac
done

shift $((OPTIND - 1))

echo "verbose=$verbose output=$output"
echo "Remaining arguments: $*"

Example run:

$ ./script.sh -v -o result.txt input1 input2
verbose=1 output=result.txt
Remaining arguments: input1 input2

OPTARG and OPTIND

getopts updates two variables automatically as it parses.

  • OPTARG: the argument value of an option that takes one (o:). Above, it is result.txt from -o result.txt.
  • OPTIND: the index of the next argument to process. It starts at 1. After the loop it points at the first non-option argument.

Running shift $((OPTIND - 1)) after the loop removes every processed option, leaving only the non-option arguments (file names, etc.) in $1 onward.

OPTIND is initialized to 1 when the shell starts. In a function that runs the getopts loop more than once in the same shell, reset OPTIND=1 before the loop or the second pass will misbehave.

How do you handle errors?

Conclusion: A leading : in optstring enables silent mode; invalid options put ? in name, missing arguments put :, and OPTARG holds the offending letter.

getopts has two error-reporting modes.

Normal mode (optstring does not start with :)

On an invalid option or missing argument, getopts prints a message to standard error itself and stores ? in name. Convenient, but you cannot control the wording.

Silent mode (optstring starts with :)

Start the optstring with a leading : (e.g. ":vo:") and getopts prints no message, letting your script handle every error. The behavior is:

Situation Value in name Value in OPTARG
Invalid option ? the offending character
Missing argument : the option letter that needed one

Error handling in silent mode:

while getopts ":vo:" opt; do
  case "$opt" in
    v) verbose=1 ;;
    o) output="$OPTARG" ;;
    \?) echo "Unknown option: -$OPTARG" >&2; exit 1 ;;
    :)  echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
  esac
done

Prefer silent mode in practice. You get full control of user-facing output: localize messages, call a usage() helper, and exit cleanly.

How does getopts differ from getopt?

Conclusion: getopts is a builtin for short options only; if you need long options like --verbose, use the external getopt (util-linux).

They differ by one letter but are different tools.

Aspect getopts (builtin) getopt (external)
What it is Shell builtin /usr/bin/getopt (util-linux, etc.)
Long options No (-a only) Yes (--all)
Portability High, POSIX-standard Varies (GNU enhanced version may be needed)
Usage Fetch one at a time in a loop Reorder all args, then parse

If short options are enough, use getopts; if --output-style long options are a requirement, consider getopt (the GNU enhanced version).

A trick exists to handle long options with getopts alone (treat - as an argument-taking option and re-parse OPTARG), but it hurts readability. When the requirement is firm, plain getopt is easier to maintain.

What are the common pitfalls?

Conclusion: Forgetting to reset OPTIND, forgetting the shift, and passing long options are the classic three.

1. Not resetting OPTIND inside a function

Using getopts in a function carries the previous OPTIND value over. Add local OPTIND (or OPTIND=1) before the loop.

parse_args() {
  local OPTIND   # function-local, reset each call
  while getopts ":vo:" opt; do
    # ...
  done
}

2. Forgetting shift and misaligning positional arguments

Skip shift $((OPTIND - 1)) and $1 stays as an option string, breaking later file handling. Always run it right after the loop.

3. Passing long options

Pass --verbose to getopts and it interprets each character after - (-, v, e...) as a separate option, causing unintended behavior. If you need long options, choose getopt at design time.

Summary

getopts implements shell option parsing in a standardized, portable way as a builtin. Memorize the while getopts ":a:bc" opt; do case ... done form, handle errors yourself in silent mode (leading :), and finish with shift $((OPTIND - 1)) to reach the positional arguments — master these three points and you are ready for real-world scripts.

Next Reading