This page (Parsing script arguments after getopts) helped me a lot in my quest to understand this. But I'll attempt to explain it all here from the ground up.
When anything runs from the terminal, it receives an array containing all the arguments it was called with. The first argument, which occupies position 0 in this array, is always the name of the program or script itself (sometimes along with its path - however it was typed in the terminal). In bash scripts we can access this array through the variables numbered $0
- $n
where n is the number of arguments we received. So for example, if our script is called like this:
./script --the -great "brown fox" --------jumped
then in our script:
$0 = ./script
$1 = --the
$2 = -great
$3 = brown fox
$4 = --------jumped
So if we do:
echo $3
we get:
brown fox
Now, there is an internal variable that the shell uses to keep track of where this array starts. All the number variables (except $0
) are offsets from the start of this array. The shift
command can be used to shift over the start of the array so that it begins one or more positions down from where it formerly did. This would make $1
reference what used to be $2
. So in our example, if we did:
shift
echo $1
echo $2
we'd get
-great
brown fox
The only exception to this whole shifting thing is $0
. $0
never moves. It will always contain the path you used to call the script.
The shift
command can also be given a number. This would be the number of positions it shifts the array. So shift 2
is equivalent to using shift
twice (i.e. the number is 1 by default).
The getopts
command has its own variable called OPTIND
that it uses to keep track of where it's up to. When getopts
successfully fetches an argument (or finishes fetching the last letter option in an argument like -abc) it adds one to OPTIND
to make it point to the next argument on the command line. The next time getopts
is invoked it will begin parsing at that next argument.
Now, here's the fun part: The number in OPTIND
also refers to an offset from the start of the array. So the shift
command can be used to affect getopts
:
#!/bin/bash
getopts "abc" arg
echo received $arg
shift
getopts "abc" arg
echo received $arg
If we run this with:
./script -a -b -c
we get:
received a
received c
What happens is this: At the start of every script OPTIND
holds the value 1. This means that when we first invoke getopts
it begins to read from argument 1, that is, the argument at position 1 from the start of the array (basically it reads $1
). After our first invocation of getopts
the OPTIND
variable holds the value 2, pointing to the -b
argument. However, then we do our shift, which makes $2
now refer to what was formerly $3
. So now, without having changed the value of OPTIND
, it now points to -c
.
How does this help us? Well, we can use the value in OPTIND
(which is accessible to us) to shift over the array skipping over all the arguments that were already parsed and making $1
the first argument that was not yet parsed. Consider this:
#!/bin/bash
while getopts "abc" arg; do
echo recieved $arg
done
shift $((OPTIND-1))
echo $1
If we run it:
./script -a -b -a -b string -a
the echo
at the bottom will always output string
no matter how many options we place before it. This is because after the while loop is done OPTIND
will always point to that string. We then use an arithmetic expansion to pass to shift
the number that is one less than the position of the string, meaning we shift $1
to its position. Thus, echoing $1
will always echo the string.
We can also continue reading options after encountering the string. The only thing to be aware of is that OPTIND
still retains its value even after the shift. So if OPTIND
is up to 5 and we shift so that $1
is the string and then we immediately go back to getopts
it will start reading at 5 arguments after the string, rather than at the argument right after, as we want it to. To take care of this we can simply set OPTIND
to 2:
OPTIND=2
or shift and then set it to 1 (so it will read what is now $1
- the argument right after the string):
shift
OPTIND=1
Putting it all together, here is some code that will read all options and make an array of all other inputs:
#!/bin/bash
while [[ $# -gt 0 ]]; do
while getopts "abc" arg; do
echo recieved $arg
done
shift $((OPTIND-1))
# when we get here we know we have either hit the end of all
# arguments, or we have come to one that is an
# input string (not an option)
# so see which it is we test if $1 is set
if [[ ${1+set} = set ]]; then
INPUTS+=("$1")
shift
fi
OPTIND=1
done
echo "Here is the array:"
echo ${INPUTS[@]}
getopt
on a non-Linux system too, but I wouldn't expect to see it on one by default. I'm also not sure if it depends on the GNU libraries to do it's thing.) – ilkkachu Aug 10 '22 at 14:02