Processing mixed operands and arguments with getopts

There are different ways of parsing arguments in Bash and choosing between GNU getopt and POSIX getopts is almost a religious issue, just like any other problem in Computer Science with more than one applicable solution. Some time ago I have discovered a simple yet interesting issue which neither of those tools is able to solve: mixing positional parameters with operands. I was surprised to find out that there is a very simple solution which seems to not be widely known. Although you can find this small tip, a SO question on exactly that problem has few not helpful answers and a real solution is either hidden in comments or at the very bottom.

The POSIX specification and guideline 9 suggest that an argument syntax should consist of a finite list of arguments followed by a finite list of operands, i.e. arguments following the last option. Sometimes we would like to abuse the syntax and insert multiple operands between options. A perfect example are build commands generated by CMake which tends to create a mix of source files or translation units, libraries to link, compilation and linking flags.

Of course it wouldn’t work directly with getopts and that’s why I have been so surprised to see it being used in a shell script which is supposed to be a wrapper invoking three different stages of building an executable. Getopts stops processing a stream of arguments as soon as first operand appears and there is no way to configure it to ignore or gather unrecognized options. You can see the problem in listing below.

while getopts <options_here> opt; do
	case "$opt" in
		<process options>
	esace
done

A loop processes recognized options as long as getopts do not fail. Let’s leave it like that and focus on getting the operand. OPTIND contains the index of the next argument to be processed. The easiest way here is to remove all already processed options by using shift. We want to remove OPTIND -1 strings to be able to access the operand as first argument $1:

FILES=""
while getopts <options_here> opt; do
	case "$opt" in
		<process options>
	esace
done
shift "$((OPTIND-1))"
FILES="$FILES $1"

The pattern above is rather known, at least on Unix Stack Exchange. To finally solve the problem we only need one more loop in which each iteration processes as many options as possible and saves the operand breaking getopts loop.

FILES=""
while [$# -gt 0]; do
	#necessary!
	OPTIND=1
	while getopts <options_here> opt; do
		case "$opt" in
			<process options>
		esace
	done
	#remove already processed arguments
	shift "$((OPTIND-1))"
	#access operand
	FILES="$FILES $1"
	#remove operand
	shift
done

This loop iterates until all arguments are processed; accessing remaining arguments with $@ is unnecessary. A crucial component is here to restart the index in each outer loop iteration. Otherwise getopts would start processing at a wrong position and it would skip a lot of valuable information.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Installing FetchContent targets in CMake
  • Google Summer of Code 2023
  • Debugging the debugger
  • Remote Bash scripts with SSH
  • JSON in Bash and CLI with jq