The Right Way to Get the Directory of a bash Script

By Wednesday, October 8, 2014 0 , , , Permalink 4

When writing bash scripts, you might want to get the directory that contains your script. There are multiple ways to accomplish that. Due to the flexibility of bash, some solutions work in some cases, but not in others.

In this post, I evolve from a naive solution to a robust and consistent solution for this common problem. Spoiler – a “good enough” middle ground that I often use is "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )", as long as I know that symbolic links are out of the game.

Getting current script directory in bash is more complex than it may seem…

First attempt: use $0

As in many other programming languages, it is possible to access the command line and arguments. In bash, $0 stores the first element of the executed command:

itamar@legolas ~ $ cat foo.sh
echo "$0"
itamar@legolas ~ $ ./foo.sh
./foo.sh

Given that, a valid strategy to get the script directory may be something like:

  1. Get directory component of $0 (using dirname).
  2. Change directory into #1 (using cd).
  3. Get full path of current directory (using pwd).

This works fine, both when running from the same directory, or from another directory:

itamar@legolas ~ $ cat foo.sh
echo "$0"
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
echo "$SCRIPT_DIR"
itamar@legolas ~ $ ./foo.sh
./foo.sh
/Users/itamar
itamar@legolas ~ $ cd foobar/
itamar@legolas foobar $ ./../foo.sh
./../foo.sh
/Users/itamar

The $( ... ) is used to execute the commands in a subshell and capture the output.

Failure of first attempt

Note that $0 stores the first element of the executed command. In the example above, that element was the path to the script, but this isn’t always the case. Obviously, when this isn’t the case, the approach will fail.

One example of such failure is with sourced scripts:

itamar@legolas foobar $ source ../foo.sh
-bash
dirname: illegal option -- b
usage: dirname path
/Users/itamar/foobar

When sourcing a script, $0 contains -bash. This makes the cd command fail, and pwd return the current directory instead of the script directory.

In bash scripts, $0 is NOT guaranteed to store the path to the current script!

Time to move on to another approach.

Second attempt: use $BASH_SOURCE

BASH_SOURCE is an array variable. From the bash documentation:

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]}

I don’t know about you, but this definition isn’t very clear to me. I want to see what it does:

itamar@legolas ~ $ cat bash_source.sh
echo "${BASH_SOURCE[*]}"
itamar@legolas ~ $ ./bash_source.sh
./bash_source.sh
itamar@legolas ~ $ source bash_source.sh
bash_source.sh

Cool. Looks like it contains the script path. Lets check it more thoroughly:

itamar@legolas ~ $ cd foobar/
itamar@legolas foobar $ cat caller_execute.sh
echo "From foobar/caller_execute: "${BASH_SOURCE[*]}""
./../bash_source.sh
itamar@legolas foobar $ ./caller_execute.sh
From foobar/caller_execute: ./caller_execute.sh
./../bash_source.sh
itamar@legolas foobar $ source caller_execute.sh
From foobar/caller_execute: caller_execute.sh
./../bash_source.sh
itamar@legolas foobar $ cat caller_source.sh

echo "From foobar/caller_source: "${BASH_SOURCE[*]}""
source ../bash_source.sh
itamar@legolas foobar $ ./caller_source.sh
From foobar/caller_source: ./caller_source.sh
../bash_source.sh ./caller_source.sh
itamar@legolas foobar $ source caller_source.sh
From foobar/caller_source: caller_source.sh
../bash_source.sh caller_source.sh

OK. So the BASH_SOURCE array consistently holds the paths to called scripts, over all combinations of execution and sourcing. Specifically, BASH_SOURCE[0] consistently stores the path of the current script. See also this article on BASH_SOURCE.

Given that, a new valid strategy to get the script directory may be to use ${BASH_SOURCE[0]} instead of $0 in the first attempt.

itamar@legolas ~ foobar $ cd

itamar@legolas ~ $ cat bar.sh
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

echo "$SCRIPT_DIR"
itamar@legolas ~ $ ./bar.sh
/Users/itamar

itamar@legolas ~ $ cd foobar/
itamar@legolas foobar $ ./../bar.sh
/Users/itamar

itamar@legolas foobar $ source ../bar.sh
/Users/itamar

Failure of second attempt

This seems good, but it fails when symbolic links are involved:

itamar@legolas ~ $ cd foobar/

itamar@legolas foobar $ ln -s ../bash_source.sh bash_source_symlink.sh
itamar@legolas foobar $ ./bash_source_symlink.sh
./bash_source_symlink.sh
itamar@legolas foobar $ source bash_source_symlink.sh
bash_source_symlink.sh
itamar@legolas foobar $ ln -s ../bar.sh baz.sh
itamar@legolas foobar $ ./baz.sh

/Users/itamar/foobar
itamar@legolas foobar $ source baz.sh
/Users/itamar/foobar

itamar@legolas foobar $ cd ..
itamar@legolas ~ $ ./foobar/bash_source_symlink.sh
./foobar/bash_source_symlink.sh
itamar@legolas ~ $ source foobar/bash_source_symlink.sh
foobar/bash_source_symlink.sh
itamar@legolas ~ $ ./foobar/baz.sh
/Users/itamar/foobar
itamar@legolas ~ $ source foobar/baz.sh
/Users/itamar/foobar

In all the failure examples above, BASH_SOURCE[0] stored the path of the symlink, not the target.

If you know that your script will never be executed or sourced via a symlink – you can stop here. This solution is simple and good enough under this assumption.

Use “$( cd “$( dirname “${BASH_SOURCE[0]}” )” && pwd )” to consistently get script directory in bash when no symlinks are involved

Final attempt: resolve symlinks in $BASH_SOURCE

The third and final solution adds symlink-resolution to the second approach.

itamar@legolas ~ $ cat bar.sh
get_script_dir () {
     SOURCE="${BASH_SOURCE[0]}"
     # While $SOURCE is a symlink, resolve it
     while [ -h "$SOURCE" ]; do
          DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
          SOURCE="$( readlink "$SOURCE" )"
          # If $SOURCE was a relative symlink (so no "/" as prefix, need to resolve it relative to the symlink base directory
          [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
     done
     DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
     echo "$DIR"
}
echo "$(get_script_dir)"
itamar@legolas ~ $ ./bar.sh
/Users/itamar
itamar@legolas ~ $ source bar.sh
/Users/itamar
itamar@legolas ~ $ ./foobar/baz.sh
/Users/itamar
itamar@legolas ~ $ source foobar/baz.sh
/Users/itamar

This is based on this excellent StackOverflow answer.

This iteration is pretty robust and consistent in the face of aliases, sourcing, and other execution variants.

Failure of the third attempt

It is not robust if you cd to a different directory before calling the function:

itamar@legolas ~ $ cat bar.sh
get_script_dir () {
     SOURCE="${BASH_SOURCE[0]}"
     while [ -h "$SOURCE" ]; do
          DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
          SOURCE="$( readlink "$SOURCE" )"
          [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
     done
     $( cd -P "$( dirname "$SOURCE" )" )
     pwd
}
cd foobar
echo "$(get_script_dir)"
itamar@legolas ~ $ ./bar.sh
/Users/itamar/foobar

Also watch out for $CDPATH gotchas..!

Summary

I explored a 3-step evolution of solutions for the same problem. Starting with a naive but inconsistent solution, and finishing with a more complex but pretty reliable one.

Even the final solution presented here isn’t perfect. bash scripting environment is very flexible, resulting scenarios that even the final solution is wrong. As always, choosing the solution to use in your script is a tradeoff between complexity, performance and correctness. I found that for me, in most cases, the solution from the second strategy is good enough.

Do you have another strategy that you use? Think you came up with a version that always works? Let me know through the comments!

No Comments Yet.

Leave a Reply