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

pathchk in Shell: Validate Path Components Before Portable Deployment

Use pathchk to check pathname validity and portability, while distinguishing character policy, component limits, and actual filesystem state.

Portable filename policy is easy to postpone until an artifact leaves the filesystem where it was created. A build may produce a name accepted by a developer’s local volume but rejected by a target filesystem, a POSIX host, an archive tool, or a deployment API. pathchk checks whether pathname operands are valid and, with portability options, whether their names satisfy conservative cross-system constraints. It is useful in release pipelines, but it is not a universal filesystem oracle, a path sanitizer, or a security boundary.

The first important distinction is between checking against the current environment and checking a portability profile. Without portability mode, implementations can compare path and component lengths with limits of the underlying filesystem and can report components that cannot be searched. Portable checks instead enforce POSIX minimum assumptions and a constrained character set. These are different questions: “Could this path exist here now?” is not “Will every supported target accept this name?”

Validate the intended name model

POSIX portable filename characters are ASCII letters, digits, period, underscore, and hyphen, plus slash as a component separator. A component should not begin with hyphen if it might later be passed as a command operand without an explicit end-of-options delimiter. The portable length limits are minimum guarantees, not maximum capabilities of every modern filesystem. A long path can be valid on the build host and still fail on a target that only promises the POSIX minimum.

GNU pathchk -p checks the portable character set and minimum length limits. GNU pathchk -P additionally rejects components beginning with hyphen, and --portability combines these policies. Those long options and some details differ among implementations; consult the target manual. A name that passes pathchk -p can still be rejected by a filesystem due to reserved names, normalization rules, case folding, network protocol constraints, or application-specific policy.

Check the final archive paths, not merely source basenames. A packaging tool may add a root directory, prefix, or generated component that pushes a path over a target limit. Likewise, an installation prefix supplied at runtime affects the full path. If the project supports multiple platforms, define a deliberately conservative naming rule and check every path after all transformations that affect it.

Use a NUL-safe manifest

Pathnames can contain newline and almost any byte other than NUL and slash within a component. Newline-delimited manifests are therefore ambiguous for arbitrary host paths. If the build format intentionally restricts names to printable ASCII without newline, document and validate that contract. Otherwise, preserve names as NUL-delimited data from traversal through validation. GNU find -print0 and xargs -0 are available widely but are not available in every historical environment; POSIX.1-2024 standardized NUL-safe find/xargs interfaces, while older systems may need a language program or a prevalidated project-specific filename policy.

For a simple generated list whose producer already enforces the portable character set, the check can be direct:

while IFS= read -r pathname; do
    pathchk -p "$pathname" || exit 1
done < package-paths.txt

This loop is safe for spaces and backslashes, but not for embedded newlines. It also checks a line-oriented list, not arbitrary filesystem byte names. Do not “fix” an error by deleting punctuation or replacing spaces without checking for collisions: two distinct source names can normalize to the same release pathname. A packaging pipeline should reject collisions explicitly before generating the archive.

Distinguish a path check from a filesystem operation

pathchk may inspect permissions and limits while validating existing path components, but the filesystem can change immediately afterward. A successful check does not reserve the name, prove that a subsequent create will succeed, or prevent a symlink race. The destination can disappear, become inaccessible, be remounted, or be replaced between validation and use. Create the file with a secure API and handle the actual result; do not use a prior pathchk success as authorization.

For a not-yet-created path, the command can determine that the name could be created under its checks even though the final component does not exist. This makes it useful before writing package metadata, but not a guarantee that disk space, quota, mount state, or access-control checks will permit a later operation. Permission and component checks answer narrow questions and should be recorded as preflight diagnostics, not as transaction guarantees.

If paths come from users, validate both syntax and meaning. Reject absolute paths or .. traversal only if the application contract requires a subtree; pathchk does not enforce containment. Resolve and open paths using descriptor-relative APIs in security-sensitive code. In shell scripts, the best mitigation is often not to accept arbitrary path syntax at all: map a validated identifier to an internally generated filename and keep extraction or deployment in a fresh, controlled directory.

Turn portability into a build gate

Run validation on the complete set of output paths using the target policy. Record which implementation and options ran, because GNU extensions can mask a portability defect. Include boundary tests at the maximum supported component and full-path lengths, non-ASCII input, leading hyphens, dot components, nested generated directories, and normalization collisions. Check archive extraction and package installation on every supported platform rather than assuming a clean build directory proves compatibility.

The useful result is a clear filename contract that upstream tools and users can follow. pathchk makes some violations visible early, but it cannot decide whether a Unicode normalization policy is right, whether a name collides on a case-insensitive volume, or whether a hostile path is safe to open. Pair it with an explicit application policy, collision detection, and the real create operation’s error handling.

Cross-platform naming is more than a character set

The POSIX portable set avoids many encoding and punctuation differences, but it does not settle every portability issue. Filesystems and APIs can apply case folding, Unicode normalization, reserved device names, trailing-dot or trailing-space rules, or maximum path limits that differ by platform. Archive tools may preserve a byte sequence that the extraction filesystem cannot represent. Even when all names are valid individually, two names may collide after a target platform’s normalization rules are applied.

If a release targets Windows, macOS, network appliances, or removable media, define a platform matrix and validate with that platform’s actual naming rules. Do not claim that pathchk -p guarantees compatibility with non-POSIX filesystems. Add collision detection after applying the target’s comparison and normalization model. For identifiers that users can choose, a restricted ASCII slug mapped to an opaque internal filename is often safer than attempting to preserve arbitrary names across every platform.

Keep validation attached to the final artifact

Build systems frequently generate or rewrite filenames after the initial source scan. Archive roots, package prefixes, version directories, and case-normalization steps all alter the final path. Run the check on the manifest that the publisher will actually consume. If the artifact contains a signed manifest, make sure any normalization step occurs before the signature is calculated and that the verifier checks the same canonical representation.

Record rejected names with an escaped diagnostic so tabs, newlines, and control bytes are visible without being interpreted by a terminal. Avoid printing raw untrusted pathnames to a terminal log; use a quoting routine from a language runtime if unusual bytes are possible. A clear report should identify the exact component, rule, target platform, and remediation without silently rewriting data.

Finally, run path validation as one step in a staging workflow. A passing name check does not mean the file was created, the directory was writable, or a later rename will succeed. Create in the intended filesystem, handle errors from the real operation, and clean up only names owned by that run. This keeps a portability preflight from being mistaken for proof of deployment success.

Make the policy inspectable

Store the accepted-name policy beside packaging code and test it with both positive and negative fixtures. Include components at the documented length boundary, a full pathname near the target limit, leading hyphens, repeated separators, spaces, non-ASCII bytes, and names that collide after case folding. A failure report should identify whether the issue is invalid syntax, a component limit, a portability profile, or a current permission problem; these categories require different remediation.

If the project intentionally permits names outside the portable set, do not run pathchk -p and then silently rewrite failing names. Keep the original logical name in metadata, define a reversible mapping to the stored artifact name, and detect collisions in that mapping. This is especially important for archives copied between platforms. Reversibility and collision checks turn a one-off workaround into a stable packaging contract that can be reviewed and tested over time.

Related:

Sources:

Comments