Consider how a while getopts
loop works even in the normal case. getopts
is called for each iteration of the loop, and even though it doesn't modify the command line args (positional parameters) themselves, it somehow needs to know where in the list of args to look at next. It has to do this by keeping "hidden" state, something not explicitly passed in the call.
This is described in Bash's reference manual:
Each time it is invoked, getopts
places the next option in the shell variable name, initializing name if it does not exist, and the index of the next argument to be processed into the variable OPTIND
. OPTIND
is initialized to 1 each time the shell or a shell script is invoked. When an option requires an argument, getopts places that argument into the variable OPTARG
. The shell does not reset OPTIND
automatically; it must be manually reset between multiple calls to getopts
within the same shell invocation if a new set of parameters is to be used.
When you source a script, it runs in the same shell environment as the main script, and this includes OPTIND
.
As hinted in the manual, just set OPTIND=1
before starting a new getopts
loop. (Don't try to unset it, it may cause issues in some shells.)
As an example, this:
set -- -a foo
while getopts a:b: option; do
echo "$option: $OPTARG"
done
set -- -b first -b second
# OPTIND=1
while getopts a:b: option; do
echo "$option: $OPTARG"
done
prints
a: foo
b: second
since the second loop continues from the position the first left off, missing the -b first
.
Uncomment the OPTIND=1
assignment in the middle, and you get the expected output:
a: foo
b: first
b: second
As for storing the arguments separately from $@
, doing args+="-a ${OPTARG} "
builds a single string, while the set of command line arguments is actually a list/array of distinct strings. The difference is most evident when any of the arguments themselves
contain whitespace. The two sets of args (-a
, foo bar
) and (-a foo
, bar
) both join into -a foo bar
, and there's no way to tell the difference from the latter string.
Instead, use an array to store the args as distinct strings.
while getopts a:b:d option
do
case "${option}"
in
a) args+=(-a "$OPTARG");;
b) args+=(-b "$OPTARG");;
d) echo 'Option:D';;
esac
done
# ...
./args.sh "${args[@]}"
(See How can we run a command stored in a variable? for further examples etc.)
Note that, the standard .
(dot) command for sourcing a script does not take arguments, but instead the sourced script sees the same $@
as the main script. (And changes made to $@
in the sourced script are visible in the main script.)
While Bash's .
/source
supports passing a new set of args, if you call source filename
without a set of args, the sourced script does not get an empty list, but the args of the main script.
So, if you do this:
source ./args.sh "${args[@]}"
you may want to take care to check that args
is not empty. Or just reset the args of main script before sourcing the other one.
Of course, that will trash the original set of args, but if you need them, you could save them to another array first:
orig_args=( "$@" ) # if needed
set -- "${args[@]}"
source ./args.sh