How to Access Remaining Command-Line Arguments After getopts Parsing in Bash


1 views

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}")