11

I have a bash script a.sh that looks like this:

#!/bin/bash
echo $#
echo $1

and a script b.sh that looks like this:

#!/bin/bash
source ./a.sh

If I call ./a.sh I'm correctly getting 0 and an empty line as output. When calling ./a.sh blabla I'm getting 1 and blabla as output.

However when I call ./b.sh blabla I'm also getting 1 and blabla as output, even though no argument was passed to a.sh from within b.sh.

This seems to be related to the use of source (which I have to use since in my real use case, a.sh exports some variables). How can I avoid arguments from b.sh being propagated to a.sh? I thought about using eval $(a.sh) but this makes my echo statements in a.sh fail. I thought of using shift to consume the arguments from b.sh before calling a.sh but I don't necessarily know how many arguments there are.

5
  • It's not arguments being propagated from b to a. It's the code being sourced from b into a. That's the reason of existence of source. Otherwise, a standard execution in a subshell would be enough. Commented Apr 8, 2020 at 14:28
  • Then don't use source. #!/bin/bash ./a.sh Commented Apr 8, 2020 at 14:28
  • 2
    You do can shift before sourcing: for arg in "$@"; do shift; done Commented Apr 8, 2020 at 14:30
  • The reason I'm doing source is that I need variables exported in a.sh to be available afterward in b.sh. Commented Apr 8, 2020 at 14:34
  • So you basically want a.sh to change b.sh's environment, but also prevent a.sh from accessing b.sh's environment. How is it possible? Commented Apr 8, 2020 at 14:40

2 Answers 2

16

The root of the problem is an anomaly in how the source command works. From the bash man page, in the "Shell Builtin Commands" section:

. filename [arguments]
source filename [arguments]
[...] If any arguments are supplied, they become the positional parameters when filename is executed. Otherwise the positional parameters are unchanged.

...which means you can override the main script's arguments by supplying different arguments to the sourced script, but you can't just not pass arguments to it.

Fortunately, there's a workaround; just source the script in a context where there are no arguments:

#!/bin/bash
wrapperfunction() {
    source ./a.sh
}
wrapperfunction

Since no arguments are supplied to wrapperfunction, inside it the arg list is empty. Since a.sh's commands are run in that context, the arg list is empty there as well. And variables assigned inside a.sh are available outside the function (unless they're declared as local or something similar).

(Note: I tested this in bash, zsh, dash, and ksh93, and it works in all of them -- well, except that dash doesn't have the source command, so you have to use . instead.)

Update: I realized you can write a generic wrapper function that allows you to specify a filename as an argument:

sourceWithoutArgs() {
    local fileToSource="$1"
    shift
    source "$fileToSource"
}
sourceWithoutArgs ./a.sh

The shift command removes the filename from the function's arg list, so it's empty when the file actually gets sourced. Well, unless you passed additional arguments to the function, in which case those will be in the arg list and will get passed on... so you can actually use this function to replace both the without-args and the with-args usage of source.

(This works in bash and zsh. If you want to use it in ksh, you have to remove local; and to use it in dash, replace source with .)

Sign up to request clarification or add additional context in comments.

1 Comment

Wow. I have an sh script (not bash, so I guess it's dash) and ran in the same issue but even worse, I'd say. I'd source a file with two parameters and they were totally ignored. Using the sub-function as in your example fixed the issue at once.
4

You can even keep passing normal arguments using

source() {
    local f="${1}"; shift;
    builtin source "${f}" "${@}"
}

It is also possible to check from the sourced file what arguments have actually been given

# in x.bash, a file meant to be sourced
# fix `source` arguments
__ARGV=( "${@}" )
__shopts=$( shopt -p ) # save shopt
shopt -u extdebug
shopt -s extdebug # create BASH_ARGV
# no args have been given to `source x.bash`
if [[ ${BASH_ARGV[0]} == "${BASH_SOURCE[0]}" ]]; then
  __ARGV=() # clear `${__ARGV[@]}`
fi
eval "${__shopts}" # restore shopt
unset __shopts
# Actual args are in ${__ARGV[@]}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.