choosing between $0 and BASH_SOURCE
Asked Answered
A

5

251

How does one choose between "$0" and "${BASH_SOURCE[0]}"

This description from GNU didn't help me much.

    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]}
Ataghan answered 26/1, 2016 at 3:10 Comment(3)
BASH_SOURCE was added at bash-3.0-alpha. You may not have it, depending on your testing regime. I found it missing on both early Solaris and OS X. Also see return: can only `return' from a function or sourced script on U&L.SE.Bituminize
does anyone else think that 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]} is complete gibberish? I have no idea what that means...would be nice if someone explained what the docs/man page actually is trying to say.Megacycle
The docs make more sense when I realized they were describing the bits and pieces necessary to construct a stack trace. Bash doesn't have a stack trace library itself (that I could find). Instead, I had to use FUNCNAME and BASH_SOURCE to build my own...Veron
S
352

Note: For a POSIX-compliant solution, see this answer.

${BASH_SOURCE[0]} (or, more simply, $BASH_SOURCE[1] ) contains the (potentially relative) path of the containing script in all invocation scenarios, notably also when the script is sourced, which is not true for $0.

Furthermore, as Charles Duffy points out, $0 can be set to an arbitrary value by the caller.
On the flip side, $BASH_SOURCE can be empty, if no named file is involved; e.g.:
echo 'echo "[$BASH_SOURCE]"' | bash

The following example illustrates this:

Script foo:

#!/bin/bash
echo "[$0] vs. [${BASH_SOURCE[0]}]"

$ bash ./foo
[./foo] vs. [./foo]

$ ./foo
[./foo] vs. [./foo]

$ . ./foo
[bash] vs. [./foo]

$0 is part of the POSIX shell specification, whereas BASH_SOURCE, as the name suggests, is Bash-specific.


[1] Optional reading: ${BASH_SOURCE[0]} vs. $BASH_SOURCE:

Bash allows you to reference element 0 of an array variable using scalar notation: instead of writing ${arr[0]}, you can write $arr; in other words: if you reference the variable as if it were a scalar, you get the element at index 0.

Using this feature obscures the fact that $arr is an array, which is why popular shell-code linter shellcheck.net issues the following warning (as of this writing):

SC2128: Expanding an array without an index only gives the first element.

On a side note: While this warning is helpful, it could be more precise, because you won't necessarily get the first element: It is specifically the element at index 0 that is returned, so if the first element has a higher index - which is possible in Bash - you'll get the empty string; try a[1]='hi'; echo "$a".
(By contrast, zsh, ever the renegade, returns all elements as a single string, separated with the first char. stored in $IFS, which is a space by default).

You may choose to eschew this feature due to its obscurity, but it works predictably and, pragmatically speaking, you'll rarely, if ever, need to access indices other than 0 of array variable ${BASH_SOURCE[@]}.


Optional reading, part 2: Under what conditions does the BASH_SOURCE array variable actually contain multiple elements?:

BASH_SOURCE only has multiple entries if function calls are involved, in which case its elements parallel the FUNCNAME array that contains all function names currently on the call stack.

That is, inside a function, ${FUNCNAME[0]} contains the name of the executing function, and ${BASH_SOURCE[0]} contains the path of the script file in which that function is defined, ${FUNCNAME[1]} contains the name of the function from which the currently executing function was called, if applicable, and so on.

If a given function was invoked directly from the top-level scope in the script file that defined the function at level $i of the call stack, ${FUNCNAME[$i+1]} contains:

  • main (a pseudo function name), if the script file was invoked directly (e.g., ./script)

  • source (a pseudo function name), if the script file was sourced (e.g. source ./script or . ./script).

Semipostal answered 26/1, 2016 at 3:15 Comment(5)
so $BASH_SOURCE is more generic and works in more circumstances?Analog
@AlexanderMills Yes, if you're using Bash, $BASH_SOURCE is the better choice.Semipostal
@CharlieParker, please see the new bottom section I've added to the answer.Semipostal
"(By contrast, zsh, ever the renegade, indeed does return the first element, irrespective of its index)." Actually, in zsh, an unsubscripted array parameter expansion becomes the whole array, not just the first item. Without shwordsplit set,$ary ~= "${ary[@]}", "$ary" ~= "${ary[*]}", and $=ary ~= ${ary[*]} or ${ary[@]} without the quotes.Shockley
Thanks, @MarkReed - I've updated the answer to fix the incorrect statement, but I didn't go into as much depth as your comment does, given that the original comment was just an aside.Semipostal
E
58

These scripts may help illustrate. The outer script calls the middle script, which calls the inner script:

$ cat outer.sh
#!/usr/bin/env bash
./middle.sh
$ cat middle.sh
#!/usr/bin/env bash
./inner.sh
$ cat inner.sh
#!/usr/bin/env bash
echo "\$0 = '$0'"
echo "\${BASH_SOURCE[0]} = '${BASH_SOURCE[0]}'"
echo "\${BASH_SOURCE[1]} = '${BASH_SOURCE[1]}'"
echo "\${BASH_SOURCE[2]} = '${BASH_SOURCE[2]}'"
$ ./outer.sh
$0 = './inner.sh'
$BASH_SOURCE[0] = './inner.sh'
$BASH_SOURCE[1] = ''
$BASH_SOURCE[2] = ''

However, if we change the script calls to source statements:

$ cat outer.sh
#!/usr/bin/env bash
source ./middle.sh
$ cat middle.sh
#!/usr/bin/env bash
source ./inner.sh
$ cat inner.sh
#!/usr/bin/env bash
echo "\$0 = '$0'"
echo "\${BASH_SOURCE[0]} = '${BASH_SOURCE[0]}'"
echo "\${BASH_SOURCE[1]} = '${BASH_SOURCE[1]}'"
echo "\${BASH_SOURCE[2]} = '${BASH_SOURCE[2]}'"
$ ./outer.sh
$0 = './outer.sh'
$BASH_SOURCE[0] = './inner.sh'
$BASH_SOURCE[1] = './middle.sh'
$BASH_SOURCE[2] = './outer.sh'
Eachelle answered 22/4, 2019 at 17:47 Comment(0)
P
28

For portability, use ${BASH_SOURCE[0]} when it is defined, and $0 otherwise. That gives

${BASH_SOURCE[0]:-$0}

Notably, in say zsh, the $0 does contain correct filepath even if the script is sourced.

Perk answered 24/7, 2020 at 11:16 Comment(4)
It depends what you want it for. $BASH_SOURCE[-1] is probably more equivalent to $0 if you want the name of the script that is running rather than the file which has been sourced.Countercheck
@TomTanner If we were considering solely bash, then your comment would be correct. But this is about compatibility with non-Bash shells. The whole point of this trick is that ${BASH_SOURCE[0]} gets evaluated to non-empty string (and becomes the result of the entire expression) in Bash, while the $0 only gets evaluated if the shell is not Bash (and does not define BASH_SOURCE). In ZSH, the equivalent of ${BASH_SOURCE[0]} is $0.Perk
Yes, but just pointing out, ${BASH_SOURCE[-1]} is the equivalent of $0 in bash, ksh and (bourne) sh. I've run across this particular issue recently. ${BASH_SOURCE[0]} in a "source"d file refers to the name of the sourced file, which isn't always what you wantCountercheck
I did not test other shells than bash and zsh, so maybe the 'compatibility maneuver' that I intend can be done in a still more portable wayPerk
B
28

TL;DR I'd recommend using ${BASH_SOURCE:-$0} as the most universal variant.

Previous answers are good but they do not mention one caveat of using ${BASH_SOURCE[0]} directly: if you invoke the script as sh's argument and your sh is not aliased to bash (in my case, on Ubuntu 16.04.5 LTS, it was linked to dash), it may fail with BASH_SOURCE variable being empty/undefined. Here's an example:

t.sh:

#!/usr/bin/env bash

echo "\$0:                     [$0]"
echo "\$BASH_SOURCE:           [$BASH_SOURCE]"
echo "\$BASH_SOURCE or \$0:    [${BASH_SOURCE:-$0}]"
echo "\$BASH_SOURCE[0] or \$0: [${BASH_SOURCE[0]:-$0}]"

(Successfully) runs:

$ ./t.sh
$0:                    [./t.sh]
$BASH_SOURCE:          [./t.sh]
$BASH_SOURCE or $0:    [./t.sh]
$BASH_SOURCE[0] or $0: [./t.sh]

$ source ./t.sh
$0:                    [/bin/bash]
$BASH_SOURCE:          [./t.sh]
$BASH_SOURCE or $0:    [./t.sh]
$BASH_SOURCE[0] or $0: [./t.sh]

$ bash t.sh
$0:                    [t.sh]
$BASH_SOURCE:          [t.sh]
$BASH_SOURCE or $0:    [t.sh]
$BASH_SOURCE[0] or $0: [t.sh]

And finally:

$ sh t.sh
$0:                    [t.sh]
$BASH_SOURCE:          []
$BASH_SOURCE or $0:    [t.sh]
t.sh: 6: t.sh: Bad substitution

Resume

As you see, only the third variant: ${BASH_SOURCE:-$0} - works and gives consistent result under all invocation scenarios. Note that we take advantage of bash's feature of making a reference to an unsubscripted array variable equal to the first array element.

Bang answered 13/7, 2021 at 9:34 Comment(5)
Using the unsubscripted array is a good idea, however using a bash feature in a shell script looking for POSIX compliance seems not very wise. It implies that you'll have different behaviors when sourced depending on the executing shell, thus it seems preferable to avoid that here and stick with $0 in POSIX 'mode', no ?Motherhood
@Motherhood Actually, I'm not seeking for POSIX compliance, there is another answer for it, I just offer the way to make it work under most probable (as I understand it) circumstances, not caring what shell dialect is actually hidden under /bin/sh (still, hoping, it'll be bash :)Bang
sh t.sh get fixed now. bash version 5.1.16Ellsworth
By using /bin/sh, you are seeking POSIX compliance.Capriccio
This is what I was looking for. It is the only setup which allows me to call my script from any working directory and have nested bash sources correctly resolve themselves. Thanks!Mantelet
P
0

I always use $0, $BASH_SOURCE can be confusing.

Adding this to top of script file to ensure calling 'return' or 'exit' correctly because 'exit' in a sourced script may go to unintended consequence as it may make terminal quit.

# Ensure to use exit safely later
if [[ $BASH_SOURCE != $0 ]]; then
    echo "Don't source this file, Bash it."
    return
fi
Principium answered 22/2 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.