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-fis a valid option, and it requires an argument. The colon afterfsignifies this.vmeans-vis 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,getoptswould print its own error messages for invalid options or missing arguments, which we often want to handle ourselves in thecasestatement.
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),$OPTARGis unset or empty. - If an invalid option is encountered,
optbecomes?and$OPTARGcontains the invalid option character. - If an option requiring an argument is given without one,
optbecomes:and$OPTARGcontains 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.