When using getopts
in Bash scripts, it processes all options and their arguments but leaves the remaining positional arguments untouched. After getopts
completes, the next argument to be processed will be stored in $OPTIND
, which holds the index of the next argument to be processed.
Here's the standard pattern to capture unprocessed arguments:
#!/bin/bash
while getopts ":ab:c:d" opt; do
case $opt in
a) echo "Option -a" ;;
b) echo "Option -b with parameter $OPTARG" ;;
c) echo "Option -c with parameter $OPTARG" ;;
d) echo "Option -d" ;;
\?) echo "Invalid option: -$OPTARG" >&2 ;;
esac
done
shift $((OPTIND-1))
echo "Remaining arguments:"
for arg in "$@"; do
echo "> $arg"
done
The critical line shift $((OPTIND-1))
moves the positional parameters so that $1
points to the first non-option argument. Here's what happens:
OPTIND
tracks the next argument index to process- After processing options,
OPTIND
points to the first non-option argument shift N
removes N arguments from the front
Consider this test case with mixed options and arguments:
$ ./script.sh -a -b foo -c bar file1.txt "file with spaces.txt"
The script would output:
Option -a
Option -b with parameter foo
Option -c with parameter bar
Remaining arguments:
> file1.txt
> file with spaces.txt
Some special cases to consider:
#!/bin/bash
# Case 1: Double dash (--) terminates option processing
# Usage: script.sh -a -b foo -- file1 file2
# All arguments after -- are treated as non-options
# Case 2: Options after non-options
# getopts stops at first non-option argument
# So script.sh file1 -a -b won't process -a and -b
# Case 3: Empty remaining arguments
if [ $# -eq 0 ]; then
echo "No remaining arguments"
fi
For more sophisticated needs, consider using getopt
(not getopts) which supports long options and more flexible argument ordering:
args=$(getopt -o ab:c:d -- "$@") || exit
eval set -- "$args"
while true; do
case "$1" in
-a) echo "Option a"; shift ;;
-b) echo "Option b with $2"; shift 2 ;;
-c) echo "Option c with $2"; shift 2 ;;
-d) echo "Option d"; shift ;;
--) shift; break ;;
esac
done
echo "Remaining args: $@"
When using getopts
in bash scripts, we often need to access arguments that aren't part of the options processing. These remaining arguments typically represent filenames, commands, or other positional parameters.
The key is the special variable OPTIND
, which getopts maintains to track the next argument to process. After your while loop completes, you can use:
shift $((OPTIND-1))
remaining_args=("$@")
Here's a full implementation:
#!/bin/bash
# Process options
while getopts "ab:c:d" opt; do
case $opt in
a) echo "Option -a triggered";;
b) echo "Option -b with parameter $OPTARG";;
c) echo "Option -c with parameter $OPTARG";;
d) echo "Option -d triggered";;
\?) echo "Invalid option: -$OPTARG" >&2;;
esac
done
# Shift to get remaining arguments
shift $((OPTIND-1))
# Store remaining arguments
remaining_args=("$@")
echo "Remaining arguments:"
printf " %s\n" "${remaining_args[@]}"
For more robust handling:
# Ensure we don't shift if no options were provided
if [ $OPTIND -eq 1 ]; then
remaining_args=("$@")
else
shift $((OPTIND-1))
remaining_args=("$@")
fi
If you're using bash 4.3+, you can use array slicing:
remaining_args=("${@:OPTIND}")