Skip to content
Shell & TerminalDeep Dive Published Updated 5 min readViews unavailable

readlink and realpath: Resolving Paths Without Hiding Symlink Semantics

Choose between reading a symlink target and canonicalizing a path, accounting for missing components, portability, newline handling, and resolution races.

readlink and realpath answer different path questions. In basic mode, readlink prints the stored target text of a symbolic link; it does not necessarily resolve that text into an absolute canonical pathname. realpath, or an implementation-specific canonicalization mode, resolves components and symlinks according to explicit rules. Confusing these operations causes broken script-directory calculations, incorrect containment checks, and assumptions that a printed path is a durable reference to one filesystem object.

POSIX.1-2024 specifies both readlink and realpath, but options and default handling of a missing final component still need attention. GNU readlink offers -f, -e, and -m variants; those are GNU behavior and not a universal substitute for standard realpath options. Choose whether every component must exist and test the operating systems in the support matrix.

Suppose /opt/current points to releases/2026-10. Basic readlink reports the literal link target, which may be relative to the symlink’s parent directory. That string alone is not a canonical absolute path. If the target is relative, resolving it requires the directory containing the link. A target may itself contain another symlink, dot components, or a path that no longer exists.

GNU realpath produces an absolute canonical name without redundant separators, dot components, or symlink components. POSIX realpath distinguishes -e, which requires the final object to exist, and -E, which permits a missing final component when the existing prefix can be resolved. The standard advises portable applications to specify one of these instead of relying on unspecified default behavior. GNU readlink’s -f, -e, and -m have different missing-component semantics; read its versioned manual before depending on them.

The option name -f is especially nonportable: on GNU readlink it means canonicalize; elsewhere the same letter may mean something else or be unsupported. Prefer the standard realpath utility with explicit -e or -E when available, and declare an alternate implementation for older systems.

A printed path is still text with framing limits

Command substitution removes trailing newline characters. Therefore a pathname whose final bytes include newline cannot be represented losslessly by a simple assignment such as resolved=$(realpath -e “$path”). POSIX realpath writes newline-terminated output and encourages implementations to treat embedded-newline pathnames as an error; GNU readlink supports a NUL output mode for some cases. Shell variables cannot contain NUL, so a general arbitrary-path protocol needs a NUL-aware consumer or an API that operates on file descriptors rather than printed names.

For ordinary application paths created under a restricted naming policy, command substitution is convenient, but state that policy. Do not use eval on printed pathnames or split them on whitespace. Quote the variable when reused, and distinguish an empty result from failed resolution by checking status:

if resolved=$(realpath -e "$candidate"); then
    printf 'resolved path: %s\n' "$resolved"
else
    printf '%s\n' 'path does not resolve under the selected policy' >&2
    exit 1
fi

This relies on an implementation providing the POSIX Issue 8 realpath flags. Check the installed utility and fail clearly on older systems rather than silently changing semantics.

Canonicalization is not a security boundary

Resolving a path and checking that it begins with /srv/app does not guarantee a later open stays inside that directory. A symlink or directory can change between canonicalization and use. Text-prefix checks are also wrong for sibling paths such as /srv/application when allowed root is /srv/app; path components must be compared, not raw string prefixes.

If a security decision depends on containment, open the object relative to a trusted directory descriptor with APIs that prevent unwanted symlink traversal. A shell path check can support operator diagnostics but cannot eliminate time-of-check/time-of-use races. Do not authorize writes solely because realpath returned an in-tree string earlier in the script.

Canonicalization also changes the meaning of relative paths. A deployment may want the lexical path requested by the user, the symlink object itself, or the final referent. These are distinct identities. For backup tools, package managers, and build systems, resolving links can collapse aliases or cross filesystem boundaries. Keep both original and canonical forms if logs or policy need to preserve the operator’s input.

Test direct links, relative links, absolute links, chains, dangling links, loops, permission-denied parents, missing intermediate components, and trailing slashes. A trailing slash generally asserts directory-like resolution; handling can expose errors for regular files. Compare behavior under each supported implementation and option.

A relative symlink target is interpreted from the directory containing the symlink, not the caller’s current directory. When constructing a new relative link, compute its target relative to the link’s parent. When resolving user input, decide whether a dangling final link is acceptable. The right answer differs for “where would this future output be placed?” and “which existing executable will run?”

Do not recursively call readlink on target output without cycle detection and an explicit maximum depth. Let the platform resolver detect loops and report failure. Preserve diagnostic text separately from path output so a warning cannot be mistaken for a pathname.

Cross-platform script design

Check whether realpath is installed and which flags it accepts. POSIX Issue 8 standardized the utility recently, but many deployed operating systems may ship older userlands. If supporting older macOS or Unix releases, provide a tested compatibility routine or small language helper using that platform’s filesystem APIs. Avoid brittle cd tricks that only work for directories when a path may refer to a file, and avoid assuming GNU readlink -f exists everywhere.

In tests, place the script in a directory whose path contains spaces, make it accessible through a relative symlink, and exercise relative and absolute invocations. Verify behavior when the script itself is a symlink and when its target is replaced. If the path locates sibling assets, decide whether those assets follow the symlink’s location or the real script’s directory.

Use readlink when target text is the data of interest. Use realpath when canonicalization is intended and existence policy is explicit. Treat output as framed text, keep race limits clear, and use descriptor-based APIs for security-sensitive path containment.

Related:

Sources:

Comments