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.
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:
- Get directory component of
$0
(usingdirname
). - Change directory into #1 (using
cd
). - 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.
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.
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!
Leave a Reply