getopts is a shell built-in that lets you parse command-line arguments in a way that feels like native Unix utilities.

Let’s see it in action with a simple script that accepts a -f flag for a filename and a -v flag for verbosity.

#!/bin/bash

verbose=0
filename=""

while getopts ":f:v" opt; do
  case $opt in
    f)
      filename="$OPTARG"
      echo "Filename set to: $filename"
      ;;
    v)
      verbose=1
      echo "Verbose mode enabled."
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument." >&2
      exit 1
      ;;
  esac
done

shift $((OPTIND-1))

echo "Remaining arguments: $@"
if [ "$verbose" -eq 1 ]; then
  echo "This is a verbose output."
fi

If you save this as my_script.sh and run it like this:

bash my_script.sh -v -f my_document.txt arg1 arg2

The output will be:

Verbose mode enabled.
Filename set to: my_document.txt
Remaining arguments: arg1 arg2
This is a verbose output.

The core of getopts is the while loop. It iterates through the command-line arguments provided to the script. getopts ":f:v" opt is the magic. The first argument, ":f:v", is the optstring.

The optstring defines the valid options and whether they require arguments.

  • f: means -f is a valid option, and it requires an argument. The colon after f signifies this.
  • v means -v is a valid option, and it does not require an argument.
  • The leading colon : in ":f:v" is crucial. It enables silent error reporting. Without it, getopts would print its own error messages for invalid options or missing arguments, which we often want to handle ourselves in the case statement.

Inside the loop, opt is set to the current option character (e.g., f, v).

  • If an option requires an argument (like -f), the argument’s value is stored in the special shell variable $OPTARG.
  • If an option doesn’t require an argument (like -v), $OPTARG is unset or empty.
  • If an invalid option is encountered, opt becomes ? and $OPTARG contains the invalid option character.
  • If an option requiring an argument is given without one, opt becomes : and $OPTARG contains the option character that was missing its argument.

The case $opt in ... esac block handles each option. We check the value of $opt and perform actions accordingly. We also have cases for \? and : to catch and report errors gracefully.

After the while loop finishes, OPTIND holds the index of the next argument to be processed. shift $((OPTIND-1)) removes all the processed options and their arguments from the positional parameters ($1, $2, etc.), leaving only the "free" arguments that weren’t part of any option. This is why arg1 arg2 are captured by $@ in the example.

The most surprising thing about getopts is how it handles bundled options and their arguments. If you have an option that requires an argument, like -f filename, and you bundle it with another option, like -v, it must be the last one in the bundle if it has an argument. For example, my_script.sh -vf my_document.txt would fail because getopts sees -v as an option, then tries to interpret f as its argument. The correct way to bundle is my_script.sh -v -f my_document.txt or my_script.sh -f my_document.txt -v. However, if an option doesn’t take an argument, it can be bundled freely: my_script.sh -vf (if f didn’t require an argument). The OPTARG variable is only populated when getopts explicitly finds an argument following an option flag, not when it’s implicitly passed as the next bundled flag.

The next concept you’ll encounter is handling options that can appear multiple times, like -i include_path, where you might want to collect all specified paths.

Want structured learning?

Take the full Bash course →