1. Introduction

Sometimes we want to manipulate a script from within its code. For example, we might have to source a script not meant for execution. We could even change its code. For that, we need the script’s location.

In this article, we discuss how we can acquire a Bash script’s location from inside that script. We begin by examining different methods of running Bash scripts. After that, we look at the context consequences of different run modes. Finally, we check how we can get the script’s location and refine it in each case. In the end, we show a universal solution, combining all discussed approaches.

We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It is POSIX-compliant and should work in any such environment.

2. Bash Scripts

For the examples in this section, we’ll use var.sh with the following content:

#!/usr/bin/env bash
declare -a value=666
echo "Inside script: value=${value}"

There are two major ways to use a script in Bash – source and execute. Sourcing runs all commands in the current shell, while execution runs them in a new shell. Let’s briefly discuss each method of interpreting the script.

2.1. Sourcing

To source a script, we can use the source or . (Dot) commands. The only difference between the two sourcing commands is that source is a Bash alias for the POSIX standard dot command:

$ declare -a value=0
$ echo "${value}"
0
$ source var.sh
Inside script: value=666
$ echo "${value}"
666

Note how the variable value in our current shell takes on the content that we assigned inside the script. This means we made changes to our current session. Because of this impact, sourcing is the less common way of running a Bash script manually in day-to-day activities.

On the other hand, the source is the foundation for most of the added functionality to Bash. For example, at the very least, the default /etc/profile Bash configuration file usually sources the current user’s bashrc files. In fact, /etc/profile itself is sourced.

In reality, users more often execute rather than source scripts by hand. Let’s see how that happens.

2.2. Executing

To execute a script, we can use the bash command or invoke the script directly by name after ensuring execution is enabled via chmod:

$ declare -a value=0
$ bash var.sh
Inside script: value=666
$ echo "${value}"
0
$ chmod +x var.sh
$ ./var.sh
Inside script: value=666
$ echo "${value}"
0

Here, as we expect, executing var.sh does not modify the value variable in our current shell.

Let’s explore how the sourcing and execution contexts differ.

3. Bash Script Context

Sourcing a script is equivalent to copying and pasting its contents into the interactive prompt. Consequently, any context we read during this process would be coming from the current shell.

On the other hand, executing a script is equivalent to spawning bash with the -c flag for a single command.

Considering this, we can compare the context of commands in both run mode environments simply:

$ echo "Current shell: \$=${$}"
Current shell: $=666
$ bash -c -- 'echo "Subshell: \$=${$}"'
Subshell: $=667
$ echo "Current shell: \$=${$}"
Current shell: $=666
$ bash -c -- 'echo "Subshell: \$=${$}"'
Subshell: $=668

Note that the shell PID $$ consistently remains 666 for the current shell. However, it changes on each execution of a new subshell with bash.

What other changes might we expect? Since main shells and subshells are obviously different processes, they can have different configurations, environments, and states.

One of the environmental changes particular to running a script is the location of that script. The zeroth argument $0 to a script’s execution contains the script invocation path:

$ echo '
> echo "${0}"
> ' > location.sh
$ bash location.sh
location.sh

At least it should and often does, but there are caveats. The main one is that $0 works like this only in executed scripts, not when sourcing:

$ source location.sh
bash

When we source, $0 just specifies the interpreter we’re using. Since the source command is a built-in, not an executable, it does not add to the argument count.

Because of these limitations, it is not recommended we use $0 for our purposes. What other means do we have to get a script’s location?

4. Bash Script Location

The most robust way to get a script’s location is the BASH_SOURCE special variable:

$ echo "${BASH_SOURCE}"

$ echo '
> echo "${BASH_SOURCE}"
> ' > location.sh
$ ./location.sh
location.sh
$ . location.sh
location.sh

Element 0 of BASH_SOURCE (i.e., ${BASH_SOURCE}) contains the relative or absolute path of the script. It’s roughly equivalent to $0, but with predictable results. Note how an interactive prompt has no associated named script.

Once we acquire the script location, we may want to transform it in one of the multiple ways.

We only use sourcing for our examples from here on out, as BASH_SOURCE does not discriminate.

The script we’re writing can be sourced via a symbolic link, i.e., symlink. Since BASH_SOURCE would contain the path to the link and not the actual script path, we can resolve the link recursively via readlink. Here are the contents of location.sh to achieve this:

SCRIPT_PATH="${BASH_SOURCE}"
while [ -L "${SCRIPT_PATH}" ]; do
  TARGET="$(readlink "${SCRIPT_PATH}")"
  if [[ "${TARGET}" == /* ]]; then
    SCRIPT_PATH="$TARGET"
  else
    SCRIPT_PATH="$(dirname "${SCRIPT_PATH}")/${TARGET}"
  fi
done
echo "BASH_SOURCE=${BASH_SOURCE}"
echo "SCRIPT_PATH=${SCRIPT_PATH}"

In a loop, we use the -L flag to test whether the current script path is a symlink. While it is, we check whether it’s absolute or relative, resolve it and continue with the resolved path.

After the loop, SCRIPT_PATH points to a hard link of the script:

$ ln --symbolic --relative location.sh /subdir/location.sh
$ . location.sh
BASH_SOURCE=location.sh
SCRIPT_PATH=location.sh
$ . subdir/location.sh
BASH_SOURCE=./subdir/locationrelativelink.sh
SCRIPT_PATH=./subdir/../location.sh

We create a symlink of our script with ln (Link) and compare the results from sourcing the original and the symlink.

Of course, all of these paths are still relative. We may want to change that.

4.2. Absolute Path

A path with a leading slash is an absolute path. To resolve relative to absolute paths, we can again use readlink, this time with the -f flag:

$ echo '
> echo "$(readlink -f "${BASH_SOURCE}")"
> ' > location.sh
$ . location.sh
/location.sh

Note the leading slash, indicating an absolute file path. Once we have that, we can manipulate it further.

4.3. Script Directory

Next, we can extract only the containing directory path via dirname (Directory Name):

$ echo '
> echo "$(dirname -- "$(readlink -f "${BASH_SOURCE}")")"
> ' > location.sh
$ . location.sh
/

The script directory can itself also be a link, which we can resolve via cd (Change Directory) and its -P flag:

$ mkdir subdir
$ ln --symbolic --relative subdir subdirlink
$ echo "$(cd "subdirlink" >/dev/null 2>&1 && pwd)"
/subdirlink
$ echo "$(cd -P "subdirlink" >/dev/null 2>&1 && pwd)"
/subdir

First, we create a symlink of a new directory. After that, each time in a subshell, we check via pwd what the current directory is after changing it with and without the -P flag to cd. In the former case, we get the actual directory name.

We now have multiple ways of acquiring pieces of information about the correct script location. Let’s combine them.

4.4. Full Bash Script Location

We take all scenarios in the previous subsections into account to reach a universal solution for acquiring the location of a Bash script regardless of run mode and conditions:

SCRIPT_PATH="${BASH_SOURCE}"
while [ -L "${SCRIPT_PATH}" ]; do
  SCRIPT_DIR="$(cd -P "$(dirname "${SCRIPT_PATH}")" >/dev/null 2>&1 && pwd)"
  SCRIPT_PATH="$(readlink "${SCRIPT_PATH}")"
  [[ ${SCRIPT_PATH} != /* ]] && SCRIPT_PATH="${SCRIPT_DIR}/${SCRIPT_PATH}"
done
SCRIPT_PATH="$(readlink -f "${SCRIPT_PATH}")"
SCRIPT_DIR="$(cd -P "$(dirname -- "${SCRIPT_PATH}")" >/dev/null 2>&1 && pwd)"

Let’s create uniloc.sh in / with the above content and execute it:

$ mkdir subdir1
$ ln --relative --symbolic . subdir1/linkdir
$ ln --relative --symbolic uniloc.sh subdir1/linkdir/linkuniloc.sh
$ . subdir1/linkdir/linkdiruniloc.sh
$ echo "${SCRIPT_PATH}"
/uniloc.sh
$ echo "${SCRIPT_DIR}"
/

First, we add a subdirectory, where we create a relative symlink of the current one called linkdir. In it, we create yet another relative symlink to our script called linkdiruniloc.sh. After sourcing the script, we see the correct paths assigned to the SCRIPT_PATH and SCRIPT_DIR variables in the current shell.

Testing this solution reveals a minor pitfall: no directory changes are allowed before we run the lines above. A small price to pay, considering its catch-all nature:

5. Summary

In this tutorial, we discussed acquiring a Bash script’s location from within the script. In addition, we saw that we should consider run mode, linking, and path relativity.

In conclusion, there are multiple pitfalls to getting the correct Bash script location inside its code, but most can be avoided using the solution we presented.

Comments are closed on this article!