Home / Alt manpages / readlink(1)

  • readlink(1)
  • User command
  • linux

Canonicalise Paths Safely with readlink

A deploy symlink looks fine until you actually resolve it with readlink and find stale data underneath. It tells you the truth: either the raw stored target, or the fully resolved canonical path, whichever mode you choose. The examples use GNU readlink from coreutils 9.4, installed on this machine.

Allow about ten minutes. You need a shell and a path you can read. Every check below is an ordinary user command: no sudo, and nothing here changes the link or its target. Watch your shell redirection though, these commands read paths and print results, they do not overwrite files.

1. Check the installed command

Confirm which executable runs and record its version before you rely on option details in a script:

$ command -v readlink
/usr/bin/readlink
$ readlink --version | head -n 1
readlink (GNU coreutils) 9.4

The command takes one or more file names. With no canonicalisation option, it prints the value stored in the symlink, it does not follow the link just to report the target.

Checkpoint

command -v reports an unexpected path? Stop and inspect that installation before comparing output against this guide. A shell alias or a different implementation can behave differently.

Pass the link itself, not the file you expect it to reach:

$ readlink /path/to/current-link
../releases/2026-09-26

That output is the link's stored value. It may be relative, and it may name a path that no longer exists, both useful when auditing a deployment link or chasing a broken target. A successful exit status only means readlink read the link entry, it does not prove the destination is usable.

Quote a path that comes from a variable, so whitespace does not get split into separate arguments:

$ link_path='/path/to/current-link'
$ readlink -- "$link_path"
../releases/2026-09-26

-- marks the end of options, a useful boundary when a file name might start with a hyphen.

3. Canonicalise a path that must exist

Use --canonicalize-existing, or its short form -e, when every component has to exist:

$ readlink -e /path/to/current-link/config/app.conf
/srv/releases/2026-09-26/config/app.conf

This follows symlinks in every component, resolves relative elements, and prints an absolute canonical name. Missing final file or any missing directory, and the command returns non-zero and normally prints nothing. That strict failure is worth having before you open a configuration file or hand a path to another program.

Check the status immediately if a script needs to tell success from an empty result:

$ readlink -e /path/to/current-link/config/app.conf > /tmp/canonical-path
$ status=$?
$ printf 'readlink status: %s\n' "$status"
readlink status: 0

That temp file is only an output capture. Check it before you consume it:

$ test -s /tmp/canonical-path && cat /tmp/canonical-path
/srv/releases/2026-09-26/config/app.conf

4. Allow a missing final component

Use --canonicalize, or -f, when parent directories must exist but the last piece may not:

$ readlink -f /path/to/current-link/cache/new-index
/srv/releases/2026-09-26/cache/new-index

Every symlink is followed, and everything except the last component must exist, so this is handy for working out where a new file would land before you create it. It does not create new-index, its parent directory, or any symlink.

Do not confuse a resolved name with a safe write. If something later writes that path, check its permissions and whether overwriting an existing file is acceptable. Nothing in this guide creates or replaces anything, so there is no undo step needed for the examples.

5. Resolve a path even when parts are missing

Use --canonicalize-missing, or -m, when no component needs to exist:

$ readlink -m /path/to/not-yet-created/../cache/index
/path/to/cache/index

This still follows symlinks that can be followed and normalises the path, but it never requires files or directories to exist. Handy for displaying a planned destination or comparing names before a provisioning step.

Safety boundary

A path that -m resolves is only a name. It is not evidence the parent is present, writable, mounted where you expect, or safe from a race. Recheck the filesystem immediately before anything security-sensitive, and use the receiving program's safe file-opening mechanism where one exists.

6. Choose output delimiters for scripts

Each result ends with a newline by default. -n drops that final newline for a single value:

$ target=$(readlink -n /path/to/current-link)
$ printf 'target=[%s]\n' "$target"
target=[../releases/2026-09-26]

Command substitution already strips trailing newlines, so -n matters most when piping straight into another program or when exact bytes count.

For several names, -z separates results with NUL characters instead of newlines, which copes with names that contain embedded newlines themselves:

$ readlink -z /path/to/link-one /path/to/link-two | od -An -t x1
 2e 2e 2f 72 65 6c 65 61 73 65 73 2f 6f 6e 65 00
 2e 2e 2f 72 65 6c 65 61 73 65 73 2f 74 77 6f 00

The 00 at the end of each record is the NUL delimiter. Pair this with a NUL-aware consumer, do not pipe it into a line-oriented parser and assume the records survived intact.

Diagnose a failed lookup

Inspect the path without changing it first:

$ ls -ld /path/to/current-link /path/to/current-link/config
$ test -r /path/to/current-link/config/app.conf && echo readable
  • Need a complete existing path? Use -e.
  • Only the final component may be new? Use -f.
  • Missing parents are expected? Use -m.

A non-zero result from -e is not fixed by adding sudo if the file genuinely does not exist. Use -v or --verbose when you need error messages; the manual also lists -q and -s for suppressing most diagnostics. Keep diagnostics on while troubleshooting, silence only suits a caller that records and handles the exit status itself.

Never treat readlink as proof a path is safe to access. It reports names, it does not reserve the result, stop a symlink changing afterwards, or enforce permissions. For a privileged service, resolve and open files using a design that addresses those race conditions, and review the operation before granting elevated access.

Done means

  • Version confirmed: GNU coreutils 9.4 is the installed implementation.
  • Stored target checked: you used plain readlink to see a link's raw value.
  • Right mode chosen: -e, -f or -m matched which components may be missing.
  • Exit status checked: you did not treat printed text alone as proof of success.
  • Delimiters handled: you used -z when newline-delimited output could be ambiguous.
  • No false safety: a canonical path stayed a name to inspect, not a security guarantee.