Sourcing vs executing in Bash

| 2 min read

Checking the value of $0 allows me to source rather than execute an entire script.

Today I wrote a script checksubmissions to check submitted pull requests in the SAP-samples/devtoberfest-2021 repo related to Devtoberfest from SAP this year, specifically the Best Practices week.

In its current form, at the time of writing, the script follows a pattern that I've used for a while now

  • some general settings and possibly global variables
  • then some function definitions, including a main function definition
  • then finally, the invocation of that main function, passing on to it any parameters that were supplied when the script is invoked

For the sake of illustration, here's a super simplified script called myscript that follows that pattern:

#!/usr/bin/env bash

set -o errexit

func1() {
echo "Inside func1"
}

main() {
echo "Running main with $*"
func1
}

main "$@"

Sometimes, especially when building out scripts like this, I like to test the functions individually, from the command line if possible. In the example from today, I check every pull request each time, initiated from main like this:

main() {

getprs | while read -r number title; do

check "$number" "$title"
sleep 1

done

}

main "$@"

(Source)

But while developing, I wanted to test out the check function (source) manually on a single pull request. Of course, editing the script to do that wasn't much of an effort, but I wondered if there was another way.

What if, from the shell prompt, I could source the script, to bring the function definitions into my current environment, and then manually invoke the check function on a single pull request?

Sourcing the script as it is would have the unwanted effect of running checks on all the pull requests, because the last line in the script actually invokes main, as it's supposed to.

It turns out that it is possible to determine whether a script is being sourced just by close examination of the $0 variable.

There's also the BASH_SOURCE environment variable, which I want to look into as well (e.g. by reading this StackOverflow post: Choosing between $0 and BASH_SOURCE) but that's for another time.

First, I can replace the simple invocation:

main "$@"

with this:

if [[ $0 =~ ^-bash ]]; then
return 0
else
main "$@"
fi

When the script is executed, the value of $0 is the name of the script. But when it's sourced, it's -bash.

Now, if I implement this change in the super simplified myscript above, then this is what happens, at a Bash shell prompt.

First, to show there's nothing up my sleeve, an attempt to invoke func1 fails:

; func1
-bash: func1: command not found

Now I execute the script, and it behaves as expected:

; ./myscript hello world
running main with hello world
Inside func1

Of course, we still don't have the func1 function available to us:

; func1
-bash: func1: command not found

But what if I source the script rather than execute it? I can do that with source myscript or simply . myscript:

; source myscript

Nothing seems to happen. Which is good -- we don't get the "running main with hello world" or "Inside func1" output.

But now the definition of func1 is available, and we can run it:

; func1
Inside func1

That seems rather appealing!

This is early days, I may have missed a fundamental gotcha, but for now, I've found a way (which vaguely reminds me of Python's if __name__ == "__main__" pattern) to be able to reduce that (already small) gap even further between the interactive shell and script content.