1. Overview
In this tutorial, we’ll learn about various ways to expand relative paths in bash.
2. Differences in Absolute and Relative Paths
In Linux, paths are used to refer to a directory or a file. We specify the complete path from the root of the filesystem up to the final destination, using the first slash (/) for the root and subsequent ones as separators:
/etc/ssl/certs
/usr/bin/ls
/usr/lib/libssl.so.3
However, with relative paths, we start the paths with a file or directory name instead of a slash. This means that they are relative to the current directory:
file.txt
./file.txt
../../usr/bin/ls
Let’s understand this more clearly with an example. Say we’re in a directory /tmp/files, where we create a file called file1. Now, we can access the file with the path file1 or ./file1 in that particular directory, but not from anywhere else:
$ cd /tmp/files/
$ touch file1
$ ls ./file1
./file1
$ cd /tmp/
$ ls ./file1
ls: ./file1: No such file or directory
We can’t access the file from other directories since we’re using a relative path. Hence, we can access files via their absolute paths from anywhere, whereas relative paths depend on the current directory.
We use the single and double dots to refer to the current directory and the parent directory, respectively. We can use multiple double dots to keep going up the directory structure. Say we want to create a relative path from our home directory to the /etc/passwd file:
$ pwd
/home/baeldung
$ cat ../../etc/passwd
root:x:0:0:root:/root:/bin/sh
...
With two double dots, we reached / from /home/baeldung.
3. Resolving Relative Paths
We might need to resolve relative paths in various cases, such as passing them to programs requiring absolute paths.
3.1. Using Bash Builtins
We can create a bash function that leverages basic bash built-ins like pwd and variable substitution for our purpose. This avoids the overhead of invoking external processes:
resolve_relative_path() (
# If the path is a directory, we just need to 'cd' into it and print the new path.
if [ -d "$1" ]; then
cd "$1" || return 1
pwd
# If the path points to anything else, like a file or FIFO
elif [ -e "$1" ]; then
# Strip '/file' from '/dir/file'
# We only change the directory if the name doesn't match for the cases where
# we were passed something like 'file' without './'
if [ ! "${1%/*}" = "$1" ]; then
cd "${1%/*}" || return 1
fi
# Strip all leading slashes upto the filename
echo "$(pwd)/${1##*/}"
else
return 1 # Failure, neither file nor directory exists.
fi
)
We must wrap the function in brackets instead of curly braces so that it runs in a subshell and restores the original working directory on exit.
Note that pwd doesn’t resolve symlinks by default, so if we’re in /tmp/symlinked_dir, which is a symlink to /tmp/original_dir, then pwd will just print /tmp/symlinked_dir. We can use the -P flag to resolve directory symlinks. However, this function will still not resolve file symlinks.
Let’s test this function with a few arguments:
$ pwd
/tmp/test
$ ls
directory file1
$ resolve_relative_path file1
/tmp/test/file1
$ resolve_relative_path ..
/tmp
$ resolve_relative_path ../test/directory
/tmp/test/directory
$ resolve_relative_path /usr/lib/../bin/..
/usr
3.2. Using readlink
We can use the readlink command to resolve relative paths, including symlinks. It uses the -f flag to print the full path:
$ readlink -f /usr/../tmp/link
/tmp/real
Here /tmp/link is a symlink to the /tmp/real file.
readlink -f returns a non-zero exit-code only if it is called with a non-existent directory but returns zero if the file’s directory exists. So, readlink -f /tmp/non-existent will return 0, but readlink -f /non-existent-dir/non-existent-file will return 1
3.3. Using realpath
Finally, we have the realpath command, which behaves similarly to readlink -f:
$ realpath /tmp/non-existent; echo $?
/tmp/non-existent
0
$ realpath /non-existent-dir/non-existent-file; echo $?
realpath: /non-existent-dir: No such file or directory
1
$ realpath /tmp/link
/tmp/real
4. Conclusion
In this article, we learned about absolute and relative paths and how we can expand them in bash scripts while accounting for symlinks as well.