sunmat
sunmat

Reputation: 7248

Avoid command line arguments propagation when sourcing bash script

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.

Upvotes: 9

Views: 3423

Answers (2)

izissise
izissise

Reputation: 943

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[@]}

Upvotes: 4

Gordon Davisson
Gordon Davisson

Reputation: 125748

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 .)

Upvotes: 14

Related Questions