11

I have a small program which contains the following folder structure:

- main.sh
- lib/
  - clean.sh
  - get.sh
  - index.sh
  - test.sh

Each file contains a single function which I use in main.sh.

main.sh:

source lib/*

get_products
clean_products
make_index
test_index

In the above the first two functions work but the second two don't.

Yet if I replace source lib/* with:

source lib/get.sh
source lib/clean.sh
source lib/index.sh
source lib/test.sh

Everything works as expected.

Anyone know why source lib/* doesn't work as expected?

1
  • 2
    Not answering the question, if you want to do it in a one-liner, look at /etc/bashrc for how it uses a for loop to deal with /etc/profile.d/*.sh. If you trust the contents of lib/ it can be reduced to a one-liner: for i in lib/*.sh; do . "$i"; done
    – Rich
    Oct 4, 2017 at 16:05

3 Answers 3

21

Bash's source builtin only takes a single filename:

source filename [arguments]

Anything beyond the first parameter becomes a positional parameter to filename.

A simple illustration:

$ cat myfile
echo "param1: $1"
$ source myfile foo
param1: foo

Full output of help source

source: source filename [arguments]

Execute commands from a file in the current shell.

Read and execute commands from FILENAME in the current shell.  The
entries in $PATH are used to find the directory containing FILENAME.
If any ARGUMENTS are supplied, they become the positional parameters
when FILENAME is executed.

Exit Status:
Returns the status of the last command executed in FILENAME; fails if
FILENAME cannot be read.

(This also applies to the equivalent "dot source" builtin . which, it's worth noting, is the POSIX way and thus more portable.)

As for the seemingly contradictory behavior you are seeing you can try running main.sh after doing set -x. Seeing what statements are getting executed and when may provide a clue.

7

Bash documentation indicates that source works on a single filename:

. (a period)

. filename [arguments]

Read and execute commands from the filename argument in the current shell context. If filename ...

And the source code ... for source ... backs this up:

result = source_file (filename, (list && list->next));

Where source_file is defined in evalfile.c to call _evalfile:

rval = _evalfile (filename, flags);

and _evalfile only opens a single file:

fd = open (filename, O_RDONLY);
5

Complementing b-layer's useful answer, I would suggest never use a greedy glob expansion if you are unsure if the files of the type trying to expand are there.

When you did below there is possibility of a file (not having .sh extension) just a temporary file containing some harmful commands (e.g. rm -rf *) which could get executed (assuming they have execute permissions)

source lib/*

So always do the glob expansion with proper bound set, in your case though you could just loop on *.sh files alone

for globFile in lib/*.sh; do
    [ -f "$globFile" ] || continue
    source "$globFile"
done

Here the [ -f "$globFile" ] || continue would take care of the returning out of the loop if no glob pattern matches in the current folder i.e. equivalent of the extended shell options nullglob in bash shell.

5
  • Using process substitution with cat would also work: source <(cat lib/*.sh) Oct 4, 2017 at 15:25
  • @Xophmeister, ...for a more limited value for "work". If you tried to debug with set -x and a PS4 that puts BASH_SOURCE and LINENO in your logs, you could no longer see which file and line a given command comes from. Oct 4, 2017 at 15:47
  • 2
    @Xophmeister, ...also, a script can short-circuit its execution with return. Following that practice, any script doing that would prevent all following ones from executing. Oct 4, 2017 at 15:48
  • 1
    This is pretty close to how it's done in /etc/bashrc when it processes /etc/profile.d/*.sh.
    – Rich
    Oct 4, 2017 at 16:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .