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

basename and dirname: Extract Path Components Without Guessing

Use basename and dirname with explicit path semantics, handling trailing slashes, repeated separators, option-like operands, and symlink resolution separately.

basename and dirname perform lexical path-component operations. basename removes directory prefixes and may remove a specified suffix; dirname returns the directory portion. Neither resolves symbolic links, verifies existence, guarantees an absolute path, or authorizes access to the resulting object. Scripts often misuse them as “find the real location of this script” helpers, which requires a separate canonicalization policy.

Pathnames have root and separator rules. Leading and trailing slashes, repeated separators, a path consisting only of slashes, an empty string, and a name ending in a suffix can affect output. Test these cases instead of assuming that a simple split on slash matches the utility specification.

Lexical extraction is not filesystem resolution

Given a path such as ./releases/current/app, basename returns the final lexical component, while dirname returns the preceding component. If current is a symlink, these utilities do not follow it. If it points elsewhere, dirname still reflects the spelling the caller supplied.

That is useful when a program intentionally operates on an alias name, but not when it needs the directory containing the final executable or target. Use realpath with an explicit existence policy for canonicalization, then decide whether real path or requested spelling is the right identity. A package manager, backup tool, and script resource lookup can require different answers.

Do not use a raw string-prefix comparison after dirname or basename to enforce containment. Path components have boundaries, and a sibling directory can share a textual prefix. For security-sensitive checks, use directory descriptors and filesystem APIs that constrain resolution.

Suffix handling can be surprising

The optional suffix operand to basename is a literal suffix-removal request, not a regular expression. A name archive.tar.gz may produce archive.tar if .gz is selected; a mismatch may leave the original component unchanged. GNU basename has additional multiple-argument options, which are extensions.

Deriving extensions with basename is not a universal file-type test. Hidden names such as .profile, versioned files such as app.2, and multi-suffix archives such as .tar.gz require application-specific semantics. Avoid interpreting a suffix as proof that content has a format; inspect or validate the file itself.

Pin down edge cases with a small contract table

The POSIX utilities have defined lexical behavior for ordinary path strings, but exceptional spellings deserve separate tests. For example, a simple name has no directory prefix, a path with a trailing slash still yields its last non-slash component, and a root path remains a root path:

basename 'release.tar'        # release.tar
dirname  'release.tar'        # .
basename '/srv/releases/app/' # app
dirname  '/srv/releases/app/' # /srv/releases
basename '/'                  # /
dirname  '/'                  # /

These are string operations, not filesystem queries. The /srv/releases/app/ result does not prove that app exists or is a directory; it only follows the utility’s component rules. POSIX also treats some edge spellings specially: the result for exactly // is implementation-defined, and the result for an empty basename operand is unspecified. Decide whether those values are rejected, normalized by the caller, or accepted with implementation-specific behavior. Do not let a platform’s current output silently become an application contract.

Dot components illustrate the difference between lexical splitting and canonicalization. basename '/srv/releases/../shared' produces shared, while dirname returns /srv/releases/..; neither collapses .. or checks where it resolves. A later filesystem operation resolves components according to the live directory tree and symlinks. If that tree can change between checking and using a path, a string comparison or earlier canonicalization is not an authorization boundary.

Select a canonicalization policy explicitly

When the requirement is a canonical absolute pathname rather than a component, use an implementation with documented realpath behavior and state whether the final component must exist. POSIX Issue 8 defines realpath -e to treat a missing final component as an error and realpath -E to tolerate that specific ENOENT case when the existing prefix can be resolved. With neither option, behavior for a missing final component is not uniform across implementations. Older systems may not provide the standardized utility, so check the target platform rather than assuming that a similarly named option has the same contract.

Canonicalization still does not freeze the filesystem. A symlink or directory can change after the command returns and before a later open or write. For operations that must stay beneath a trusted directory, use APIs that apply the platform’s containment policy during the open; do not treat realpath followed by a separate operation as race-free. A directory descriptor alone is not necessarily enough to constrain .. or symlink traversal. For example, Linux openat2(2) offers RESOLVE_BENEATH and RESOLVE_IN_ROOT; callers still need to select the appropriate documented policy and handle its errors. Other systems expose different controls.

Preserve names across output boundaries

Both utilities normally print a newline after each result. A pathname may itself contain newlines, so line-oriented output is ambiguous for arbitrary names. Command substitution also removes trailing newline characters, which means component=$(basename "$path") cannot preserve a component ending in newline. Quoting prevents shell word splitting and glob expansion, but it cannot repair information already lost by the output format.

On GNU systems, basename -z and dirname -z use NUL terminators as an extension; downstream processing must stay NUL-aware, and the value cannot be stored unchanged in a shell variable. For portable shell interfaces, either impose and validate a filename policy that excludes problematic bytes or keep path handling in a language API that passes argument vectors and filesystem paths without converting them to newline-delimited text.

Option-like and unusual path operands

Quote every path variable so spaces and wildcard characters remain data. Some implementations support – to end option parsing; for portability, a relative operand beginning with a dash can be prefixed with dot-slash when that preserves the desired lexical form. Empty input should be rejected or handled explicitly rather than allowed to produce a misleading component.

Newlines are allowed in pathnames, while these utilities ordinarily print newline-terminated text. Capturing output in command substitution strips trailing newlines and cannot preserve every possible pathname byte sequence. For normal application paths, constrain naming policy and document it. For arbitrary names, use a NUL-aware tool or a language API.

Script-directory patterns need a stated policy

A common script task is locating a sibling file. Decide whether the sibling is relative to invocation path, the directory entry used to reach the script, or canonical target after resolving symlinks. These differ when a script is called through PATH, a relative path, or a symlink. basename and dirname alone do not find the executable that the shell ultimately ran.

Test invocation from another working directory, through a PATH entry, via a relative symlink, and with whitespace in the directory name. Quote intermediate results and check every command status. If deployment guarantees a fixed layout, state it and avoid overly clever resolution code. If the script must resolve symlinks portably, use a tested helper for each supported platform rather than a fragile loop.

Verify results at the filesystem boundary

Lexical path utilities do not prove that a parent exists, target is readable, or a subsequent write stays in the same directory. Check the actual operation’s status. If creating a destination, use a safe temporary-file API and set ownership and mode deliberately. If resolving an existing target, use the path canonicalizer’s explicit missing-component behavior.

Test root paths, repeated slashes, trailing slashes, empty strings, suffix mismatches, names beginning with a dash, spaces, and newlines. Keep lexical extraction and physical resolution as separate operations in code and documentation. This makes callers reason about the path they supplied instead of assuming a utility silently followed links.

Related:

Sources:

Comments