-1

I am trying to pass all parameters from one script to another. However, there is an issue when the other script is sourced; all parameters are not passing correctly.

first.sh:

#!/usr/bin/bash

while getopts a option do case "${option}" in a) echo 'OptionA:somevalue';; esac done

This way works

./second.sh "$@"

Does not work when source command is used

#source ./second.sh "$@"

second.sh:

#!/usr/bin/bash

while getopts b:c option do case "${option}" in b) echo 'OptionB:'"${OPTARG}";; c) echo 'OptionC:somevalue';; esac done

Output:

$ ./test.sh -a -b foo -c
OptionA:somevalue
./first.sh: illegal option -- b
./second.sh: illegal option -- a
OptionB:foo
OptionC:somevalue

Expected output:

$ ./test.sh -a -b foo -c
OptionA:somevalue
OptionB:foo
OptionC:somevalue

What to achieve?

Passing the parameters correctly to second.sh with source command, getting rid of illegal option and compatibility with other shells.

Edit: Updated the question again with more clear examples.

Zero
  • 13

1 Answers1

4

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
ilkkachu
  • 138,973
  • Well written and explained. I wanted to write a comment in a similar line, but would have been stuck with, multiple getopts in the same shell will ruin the functionality and can lead to a breakage if not used with care.

    Thanks for all the details and an actual explanation, so I can actually understand the behaviour I just found out ;)

    – Patrick Abraham Mar 09 '24 at 17:04
  • Your answer does't provides a solution for my question. I need to achieve this when other script are begin sourced. – Zero Mar 09 '24 at 17:37
  • @Zero, did you try what I suggested? – ilkkachu Mar 09 '24 at 18:01
  • @Zero, note that your post says "all parameters are not passing correctly", but you haven't shown what "$@" (or $1, $2...) hold in the inner script. – ilkkachu Mar 09 '24 at 18:08
  • @ilkkachu You have well explained but your code doesn't works when other script are called with source. – Zero Mar 09 '24 at 18:17
  • @ilkkachu I have already shown the parameters I want to pass in the output. I have updated my question and updated the example please check it again. – Zero Mar 09 '24 at 18:20
  • 1
    @Zero, well, I didn't give a complete code, to be honest. I tried to explain the issue and said "As hinted in the manual, just set OPTIND=1 before starting a new getopts loop." It works for me that way. You didn't say if you tried resetting OPTIND that way in your code. – ilkkachu Mar 10 '24 at 08:49
  • @ilkkachu Actually I have seen your previous answers here, you were suggesting to not reset OPTIND to avoid compatible issues. I start this question because I was worried about the compatible issues but since I have tested this with zsh I got no issues and it turns out resting the OPTIND=1 was the solution from the begging. sorry and thanks you! – Zero Mar 11 '24 at 18:41
  • @Zero, mmhmh, which previous answers? I seem to have mentioned before that the exact value of OPTIND is shell-specific, but resetting it to 1 is fine, actually sanctioned in the standard, and is something that needs to be done when restarting option processing. If I've said somewhere that one shouldn't do it, then that's been complete hogwash. (I tried to look if written something like that, but couldn't find it. But I may be just blind.) – ilkkachu Mar 11 '24 at 21:55
  • @ilkkachu Ops, You were not the one who said that I was mistaken x) but the note is here: https://stackoverflow.com/q/23581368/ – Zero Mar 11 '24 at 23:49
  • @Zero, right, there's an answer there just saying "Setting OPTIND=1 may not work reliably on zsh." without any specifics or references... Zsh's manual says "The first option to be examined may be changed by explicitly assigning to OPTIND. OPTIND has an initial value of 1, [...]". So I don't see why there should be an issue with zsh either. Plus, there's the POSIX text saying "If the application sets OPTIND to the value 1, a new set of parameters can be used [...]" and while zsh doesn't try to be bound by POSIX, it's hard to see what the point of breaking compatibility here would be... – ilkkachu Mar 12 '24 at 11:09
  • @ilkkachu Please If you don't mind check my updated question. – Zero Mar 20 '24 at 03:30
  • Beware that if doing source ./other-script "${args[@]}", if args is an empty list, other-script will see unmodified "$@". Better not to pass extra arguments to source/. (which is not portable anyway), and do set -- "${args[@]}"; source ./other-script. – Stéphane Chazelas Mar 20 '24 at 06:11
  • @Zero, mm, I'm not exactly sure what the exact requirement in your edited question is. If you want to get rid of the errors getopts prints, add a colon to the front of the option string (e.g. getopts :abc opt). But if you want to have the first script accept options for itself and the other script, I'm not sure if that's the best way. You might want to post another question about that. – ilkkachu Mar 20 '24 at 09:00
  • @ilkkachu Tbh I have got several errors when used set --' on macOS based shell, the variables somehow were not passing correctly. rest of your answer are very good. For theillegal option --errors I have ignored them by redirecting the stderr into/dev/nul` please excuse my late respond. – Zero Mar 24 '24 at 07:09
  • @Zero, well, you don't seem to be showing what those several errors are, so it's hard to say anything. Post another question and show the exact situation. – ilkkachu Mar 24 '24 at 09:01
  • @ilkkachu I have tried setting the switches into array (as you have explained here) and finally I got it working. One thing I should mention when I first tried setting the switches into array it didn't work because I wasn't resetting the OPTIND it's a mistake from my side sorry. Thanks you for the answers and I wish you the best. – Zero Mar 24 '24 at 23:17