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

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.