realpath and readlink Explained: Resolving Symlinks and Relative Paths
A path can describe the same file in many ways: relative, containing .., or passing through symlinks. realpath collapses all of that into the single canonical absolute path.
cd /var/log
realpath ../log/./nginx/access.log
# /var/log/nginx/access.log
realpath ~/.bashrc
# /home/alice/.bashrc
What it resolves
Three things at once:
- Relative paths become absolute
.and..components are collapsed- Every symlink, including ones in intermediate directories, is followed
ls -l /usr/bin/python3
# /usr/bin/python3 -> python3.13
realpath /usr/bin/python3
# /usr/bin/python3.13
That last property is what distinguishes it from string manipulation. dirname and basename never touch the filesystem, so they cannot know a symlink exists.
readlink
readlink /usr/bin/python3
# python3.13 just the immediate target
readlink -f /usr/bin/python3
# /usr/bin/python3.13 fully resolved
Plain readlink reads one link and stops, which is what you want when you care about the link itself. readlink -f behaves like realpath.
readlink -f is more portable to older systems; realpath is clearer and has more options. Use whichever your environment supports consistently.
Existence behaviour
realpath /does/not/exist
# error
realpath -m /does/not/exist
# /does/not/exist allows missing components
realpath -e /does/not/exist
# error, and -e requires the whole path to exist
-m is what you want when computing a path you are about to create. The default failing on missing paths catches people out in scripts that build output paths.
Relative output
realpath --relative-to=/home/alice /home/alice/projects/app/main.py
# projects/app/main.py
Useful when generating output that should not contain machine-specific absolute paths, such as a manifest or a report meant to be shared.
Where scripts need it
Finding the script’s own real location, even when invoked through a symlink:
SCRIPT="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT")"
Without the realpath, a script installed as /usr/local/bin/mytool pointing at /opt/mytool/bin/mytool computes its directory as /usr/local/bin and fails to find its own libraries.
Confirming two paths are the same file:
if [[ "$(realpath -m "$src")" == "$(realpath -m "$dst")" ]]; then
echo "source and destination are the same file"
exit 1
fi
That check prevents an rsync or cp that silently destroys data because two different-looking paths resolved to one place.
Validating that a path stays inside a directory, which matters when the path comes from user input:
base=$(realpath /srv/uploads)
target=$(realpath -m "/srv/uploads/$user_supplied")
case "$target" in
"$base"/*) : ;;
*) echo "path escapes the upload directory" >&2; exit 1 ;;
esac
Path traversal is the vulnerability class behind a large share of the container and archive extraction bugs that get published, and this is the shell version of the check.
Related
Our symlinks and hard links guide covers what these are resolving, and stat shows the metadata behind a path.
Frequently Asked Questions
What is the difference between realpath and readlink -f?
They do essentially the same job of resolving a path to its canonical absolute form. realpath is the clearer name and has more options; readlink -f is more widely available on older systems. Plain readlink without -f reads one link and does not resolve chains.
Why does realpath fail on a path that does not exist?
By default it requires every component to exist, because it resolves against the real filesystem. Use -m to allow missing components, or -e to require the whole path to exist, depending on which behaviour you want.
How do I get a path relative to another directory?
Use --relative-to with the base directory. This is how you turn an absolute path into one relative to a project root, which is useful when generating output that should not contain machine-specific prefixes.
Does realpath resolve every symlink in the path?
Yes, including symlinks in intermediate directories, and it follows chains of links. That is the difference from plain readlink, which reads a single link target and stops.
Why does my script behave differently when invoked through a symlink?
Because the path it sees is the symlink, not the real file, so anything derived from it points at the wrong directory. Resolving with realpath first makes the script work regardless of how it was invoked.
Is realpath available on macOS?
Not by default in older versions, which is why portable scripts often use readlink -f or a small shell function instead. Installing GNU coreutils provides it as grealpath.