One day, I was wondering how to share a function across multiple Bash scripts. So I asked a large language model (LLM) for advice. It suggested I place shared functions in a separate file and source that file from other scripts.

That made sense—modular code is reusable and avoids reinventing the wheel. The LLM also suggested I add the following guard at the top of the shared file:

# common_functions.sh
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    echo "Error: This file must be sourced, not executed." >&2
    exit 1
fi

As an intermediate Linux user, I knew that $0 is the name of the script being executed. But BASH_SOURCE? That was new to me. Time to dive into the Bash rabbit hole.

First Stop: The Manual

BASH_SOURCE
An array variable whose members are the source filenames where the corresponding shell function names in the FUNCNAME array variable are defined. The shell function \${FUNCNAME[\$i]} is defined in the file \${BASH_SOURCE[\$i]} and called from \${BASH_SOURCE[$i+1]}.

It seems that there is another array related to BASH_SOURCE, which is FUNCNAME. Judging from its name, we can guess that it might be used to keep track of the execution of functions. The man page tells us that our intuition is correct.

FUNCNAME
An array variable containing the names of all shell functions currently in the execution call stack. The element with index 0 is the name of any currently-executing shell function. The bottom-most element (the one with the highest index) is "main". This variable exists only when a shell function is executing. Assignments to FUNCNAME have no effect. If FUNCNAME is unset, it loses its special properties, even if it is subsequently reset.
This variable can be used with BASH_LINENO and BASH_SOURCE. Each element of FUNCNAME has corresponding elements in BASH_LINENO and BASH_SOURCE to describe the call stack. For instance, \${FUNCNAME[\$i]} was called from the file ${BASH_SOURCE[\$i+1]} at line number \${BASH_LINENO[\$i]}. The caller builtin displays the current call stack using this information.

Huh, it seems that there is another array related to BASH_SOURCE, which is BASH_LINENO. Although in the current section, we know what it means, but I still decided to learn more.

BASH_LINENO
An array variable whose members are the line numbers in source files where each corresponding member of FUNCNAME was invoked. \${BASH_LINENO[\$i]} is the line number in the source file (\${BASH_SOURCE[\$i+1]}) where \${FUNCNAME[\$i]} was called (or \${BASH_LINENO[\$i-1]} if referenced within another shell function). Use LINENO to obtain the current line number.

Interpreting the Call Stack

Putting it together:

  • ${FUNCNAME[$i]} is defined in ${BASH_SOURCE[$i]}.
  • It was called from ${BASH_SOURCE[$i+1]} at line ${BASH_LINENO[$i]}.

This is similar to how call stacks work in languages like C.

To illustrate, consider this C program:

// file: foo.c
#include <bar.h>

void foo(void) {
    bar();
    return;
}

int main(void) {
    foo();
    return 0;
}

// file: bar.c
void bar(void) {
    return;
}

Even though bar() is defined in bar.c, we say it's called from foo.c because it's invoked in foo().

Likewise, in Bash:

  • ${FUNCNAME[$i]} is defined in ${BASH_SOURCE[$i]}
  • and called from ${FUNCNAME[$i+1]} in ${BASH_SOURCE[$i+1]}.

Testing the Theory

I wrote three Bash scripts:

#!/bin/bash

echo "This is outer.sh"
. middle.sh
#!/bin/bash

echo "This is middle.sh"
. inner.sh
#!/bin/bash

func() {
    echo "Length of BASH_SOURCE: ${#BASH_SOURCE[@]}"
    for i in "${!BASH_SOURCE[@]}"
    do
        echo "\${BASH_SOURCE[$i]}: ${BASH_SOURCE[$i]}"
    done

    echo "Length of FUNCNAME: ${#FUNCNAME[@]}"
    for i in "${!FUNCNAME[@]}"
    do
        echo "\${FUNCNAME[$i]}: ${FUNCNAME[$i]}"
    done

    echo "Length of BASH_LINENO: ${#BASH_LINENO[@]}"
    for i in "${!BASH_LINENO[@]}"
    do
        echo "\${BASH_LINENO[$i]}: ${BASH_LINENO[$i]}"
    done
}

func

Note: we must wrap the print logic in a function. Otherwise, FUNCNAME won't be set.

Now run it:

$ ./outer.sh 
This is outer.sh
This is middle.sh
Length of BASH_SOURCE: 4
${BASH_SOURCE[0]}: inner.sh
${BASH_SOURCE[1]}: inner.sh
${BASH_SOURCE[2]}: middle.sh
${BASH_SOURCE[3]}: ./outer.sh
Length of FUNCNAME: 4
${FUNCNAME[0]}: func
${FUNCNAME[1]}: source
${FUNCNAME[2]}: source
${FUNCNAME[3]}: main
Length of BASH_LINENO: 4
${BASH_LINENO[0]}: 23
${BASH_LINENO[1]}: 4
${BASH_LINENO[2]}: 4
${BASH_LINENO[3]}: 0

Interpreting the Output

Let’s decode the call stack:

  • func is defined in inner.sh, and is called by source from inner.sh at line 33
  • source is defined in inner.sh, and is called by source from middle.sh at line 4
  • source is defined in middle.sh, and is called by main from ./outer.sh at line 4
  • main is defined in ./outer.sh, and is called by ??? from ??? at line 0

Notice the problem? source is a Bash builtin which is not defined by us, and main is called by no one from nowhere because of out of array index. What's more, main is not introduced in detail in the man page of Bash. So, the the rule we mentioned above is not exactly correct at these corner cases, but is still good enough to interpret the user-defined functions.

What Are source and main in FUNCNAME?

These names aren’t user-defined functions. They are pseudo-functions inserted by Bash itself.

source

When you use source (or .), Bash internally pushes "source" onto the call stack to track the context. It behaves as if the contents of the sourced file are being executed inside a function.

You can even see this in the Bash source code:

git clone https://git.savannah.gnu.org/git/bash.git
cd bash
grep -i "\"source\"" -nr *

Output:

builtins/evalfile.c:240:  array_push (funcname_a, "source");	/* not exactly right */
builtins/mkbuiltins.c:158:  ":", ".", "source", "break", "continue", "eval", "exec", "exit",
builtins/source.def:2:It implements the builtins "." and  "source" in Bash.
builtins/source.def:168:  begin_unwind_frame ("source");
builtins/source.def:197:  run_unwind_frame ("source");
grep: po/ga.gmo: binary file matches

Even the comment in the code admits this is an imperfect abstraction.

main

The "main" entry represents the top-level execution context—what runs when Bash starts interpreting a script. This is confirmed in shell.c:

grep -i "\"main\"" -nr *

Output:

doc/bash.0:1004:              tom-most element (the one with the  highest  index)  is  "main".
doc/bash.1:1838:.if t \f(CW"main"\fP.
doc/bash.1:1839:.if n "main".
doc/bash.info:5497:     bottom-most element (the one with the highest index) is '"main"'.
doc/bash.ps:1854:10/Courier@0 SF("main")2.884 E F0 5.384(.T)C .384(his v)-5.384 F .385
grep: doc/bashref.dvi: binary file matches
doc/bashref.info:5498:     bottom-most element (the one with the highest index) is '"main"'.
doc/bashref.ps:14195:(with)g(the)g(highest)630 1549 y(index\))e(is)h Ft("main")p
doc/bashref.texi:6396:is @code{"main"}.
make_cmd.c:801:     initialized come from the environment.  Otherwise default to "main"
make_cmd.c:804:    temp->source_file = shell_initialized ? "main" : "environment";
shell.c:1640:  array_push (funcname_a, "main");

So main isn’t a real function. It just marks the start of your script execution. That’s why it appears at the bottom of the call stack.

Conclusion

While FUNCNAME, BASH_SOURCE, and BASH_LINENO give you powerful introspection into Bash's call stack, they have some quirks:

  • FUNCNAME may include source and main, even though they're not real functions.
  • BASH_SOURCE tracks sourced files just like includes in C.
  • Always wrap logic inside functions to get useful stack traces.

Understanding these variables can help you debug, audit, or even write tools in Bash that require deep introspection.