In shell scripting, there are many situations where you want to know the path to the script file itself.
I use this frequently in scripts that build pkgs, because I want the script to access or create files relative to the script or its enclosing folder. In other words: the script sits in some project folder, and you want to get the path to that project folder.
Some scripts just use the current working directory, but since you can launch scripts from outside the current working directory, this is quite unreliable.
One example is this script from my book ‘Packaging for Apple Administrators‘ which builds an installer package for a desktop picture from resources in the same folder.
In that package build script I have the following line:
projectfolder=$(dirname "$0")
This is a useful but somewhat tricky line. It will work in most situations, but not all. To understand when it might fail, we will have to unravel it from the inside.
The $0
variable is the first element of the command line as it is entered in to the prompt and contains the command itself. In our case it will most likely be ./buildBoringDesktopPkg.sh
.
The dirname
command removes the last element from a path. This will return the path to the directory containing the item you give it. When we do dirname $0
after launching the script with ./buildBoringDesktopPkg.sh
we will get .
which is the current working directory, but also the parent directory of the script.
When you launch the build script with a different working directory, i.e. like
> Projects/BoringDesktop/buildBoringDesktopPkg.sh
The $0 argument will be Projects/BoringDesktop/buildBoringDesktopPkg.sh
the dirname
will return Projects/BoringDesktop/
. This is all good and it will still find the resources and create the pkg file in its project folder.
In most situations the syntax used to call your script will be a relative or absolute path to the script. Something like ./buildPkg.sh
or ~/Projects/Tools/my_script.sh
. When the shell ‘sees’ a slash /
in the command, it will assume this is the path to the script or executable and use that.
But when there is no slash /
in the command, the shell will use the directories listed in the PATH
environment variable to look for the command. This is the reason you can type sw_vers
instead of having to write /usr/bin/sw_vers
.
This is also the reason you have to type ./my_script.sh
to run a script you are working on. Your script is not in the PATH
, but the command contains a slash and tells the shell exactly where to look (in the current directory). When you just type my_script.sh
the shell will look in the PATH
directories, not find your script, and error out. (Unless your current directory happens to be in the PATH
.)
Note: Learn the details about
PATH
in my new book: “macOS Terminal and Shell“
So, there are a lot of pieces that are really confusing here. Let’s create a script that will help us visualize this:
#!/bin/sh
echo "pwd: $(pwd)"
echo "\$0: $0"
echo "dirname \$0: $(dirname $0)"
This script shows the values of the current working directory, the $0
argument, and the value returned by dirname $0
. Save this script as script_path_test.sh
in your Desktop folder.
> cd Desktop
> ./script_path_test.sh
pwd: /Users/armin/Desktop
$0: ./script_path_test.sh
dirname $0: .
> cd ~
> Desktop/script_path_test.sh
pwd: /Users/armin
$0: Desktop/script_path_test.sh
dirname $0: Desktop
So far, so good. While some of these paths look a little odd, they all return the expected values.
But now imagine we like our script so much, that we use it a lot. To save on typing the path to the script all the time, we add a symbolic link to it to /usr/local/bin
. This puts our script in the default PATH
(for an interactive shell) and we can now execute it without needing to type the path to it:
> sudo ln -s ~/Desktop/script_path_test.sh /usr/local/bin/script_path_test.sh
> script_path_test.sh
pwd: /Users/armin
$0: /usr/local/bin/script_path_test.sh
dirname $0: /usr/local/bin
So, this is less ideal. When you enter just script_path_test.sh
the shell finds the symbolic link in /usr/local/bin
and runs it. The $0
argument points to the symbolic link instead of the actual script and hence dirname $0
points to /usr/local/bin
which is not where the resources for our script are.
(Remember to delete this symbolic link from /usr/local/bin/
when you are done with this.)
We need a tool that resolves symbolic links.
There are quite a few solutions to this, many of which can be found in this Stack Overflow post.
GNU-based systems have a realpath
command line tool, which does exactly this. macOS does not have this tool. You can add GNU tools to macOS with many of the popular package managers, but that introduces a dependency and seems overkill.
Python has a function that does this. Add this line to the script:
echo "python realpath: $(python -c "import os; print(os.path.realpath('$0'))")"
This works fine, however, the built-in Python for macOS is deprecated. We don’t want to introduce this dependency, when it might go away in a future macOS update.
Then I found this post where I learned that zsh has a parameter expansion modifier to return the absolute path while resolving symbolic links.
Let’s modify our script to use zsh:
#!/bin/zsh
echo "pwd: $(pwd)"
echo "\$0: $0"
echo "dirname \$0: $(dirname $0)"
echo "\${0:A}: ${0:A}"
echo "dirname \${0:A}: $(dirname ${0:A})"
and then we can run our script:
> script_path_test.sh
pwd: /Users/armin
$0: /usr/local/bin/script_path_test.sh
dirname $0: /usr/local/bin
${0:A}: /Users/armin/Desktop/script_path_test.sh
dirname ${0:A}: /Users/armin/Desktop
Zsh to the rescue. We have a solution! Zsh is pre-installed on every macOS and since Apple chose it as their default shell since Catalina, it will be around for a while longer.
But what if you have sh or bash scripts that you cannot just quickly convert to zsh? The good news here is that we can create a zsh one-liner that does this, even from a bash or sh script, very similar to the python one-liner above.
echo "zsh absolute path: $(zsh -c 'echo ${0:A}' "$0")"
echo "zsh dirname absolute: $(dirname $(zsh -c 'echo ${0:A}' "$0"))"
So, in conclusion:
dirname $0
will give you the enclosing folder of the script in most situations- with zsh you can just use
dirname ${0:A}
to be even safer - when you have to use sh or bash, you can use a zsh one-liner:
dirname $(zsh -c 'echo ${0:A}' "$0")
.
Happy Scripting!
(Remember to delete the symbolic link from /usr/local/bin/
!)