June 16, 2022·5 min read·engineeringfishpathsscripting

fish 3.5 adds a path-aware builtin

The new path command distinguishes text transformations from filesystem checks and preserves filename boundaries across pipelines and substitutions.

A path is a string, but manipulating its spelling and inspecting the object it names are different operations. fish 3.5.0, released on June 16, introduces a path builtin that makes both available through a common command. The versioned release notes demonstrate a pipeline that filters files, removes an extension, extracts another extension, and deduplicates the results.

The 3.5.0 command reference makes the distinction explicit. Some subcommands operate on strings and can handle nonexistent paths. Others consult the filesystem and filter values according to what exists there. That separation is useful when composing a command whose intermediate result is a proposed name rather than a file already on disk.

I like this addition because an interactive shell benefits from concise operations that retain the meaning of their input. A path command should not make every caller reconstruct filename handling through a regular expression. It should also make clear when it is checking the filesystem instead of changing a spelling.

Text operations do not imply file operations

The basename and dirname subcommands separate the last component from its directory portion. Extension operations inspect the final dot under documented rules, excluding cases such as a dot immediately following a slash or the special basenames dot and dot-dot. Changing an extension returns another path string.

path change-extension mp4 ./recording.wmv

The command prints the corresponding path with the new extension. It does not rename the recording. That distinction allows the result to serve as an argument to a separate conversion or rename operation, with the filesystem action remaining visible in the enclosing command.

An empty extension removes the existing suffix. A leading dot in the requested extension is ignored, so the caller can specify either spelling. The reference also distinguishes a dotfile such as .config from a filename that has another extension after it. These rules are more specific than splitting every path at any dot character.

Normalization is another string-oriented operation. It collapses repeated separators and dot components without making the result absolute or resolving symbolic links. It can consequently operate on a path that does not exist. That is appropriate when the task is cleaning up a name before creating anything.

Resolution has different semantics. path resolve produces a normalized physical absolute path and resolves symbolic links where it can. The reference says that if the entire path cannot be resolved, it resolves as much as possible and normalizes the remaining portion. A printed result is therefore not an unconditional proof that every component exists.

Filters inspect the current filesystem

The filter subcommand checks paths for types or permissions. Without inversion, nonexistent paths do not pass. Type selections include files and directories, and permission selections include readability, writability, and executability for the current user.

The combination rules matter. A path can match any requested type but must satisfy all requested permissions. Selecting files or directories is sensible, while requiring one object to be both types would not be. Requiring both write and execute permissions, however, is a meaningful combined check.

path filter -f -x /usr/bin/fish /usr/bin

This asks for existing executable files among the supplied paths. The directory does not pass the file requirement. Whether the named fish executable passes depends on the machine, so the operation expresses a check rather than assuming an installation layout.

For a condition without output, path is is shorthand for quiet filtering. It returns success if any supplied path passes. The word any is important for callers handing it several paths. Success does not establish that every member met the requested condition.

The reference also describes defensive treatment of paths beginning with a minus sign. Filtering and normalization prepend a relative-path prefix where needed so a later command need not mistake that path for an option. The command still supports the usual double-hyphen boundary for its own command-line arguments.

Filename boundaries survive composition

Pathnames can contain newlines, so newline-delimited output is not a universal interchange format for them. The builtin supports NUL-delimited input and output through separate options. NUL works as a separator because Unix pathnames cannot contain it.

The reference says path automatically detects NUL input near the beginning of a stream, while explicit options remain available. The following pipeline makes both sides' delimiter choices visible.

path filter -Z -f -- * | path resolve -z

The first operation selects files and emits NUL separators. A second path command accepts that input before resolving the paths. The glob enumerates the input names, while the pipeline transforms the selected paths. A newline inside one filename does not need to become a boundary between two names.

Command substitution has its own integration. The documentation says fish automatically splits path-command output appropriately when it appears in a substitution, including names containing newlines. This is a property of the builtin's cooperation with the shell, so it should not be generalized to arbitrary external commands that print filenames.

Input selection also has a defined rule. Path arguments come from the command line unless standard input is connected to a pipe or file. Supplying paths through both channels is an error, which avoids an ambiguous combination of two independent argument sources.

Sorting also has path-specific rules. Numerical runs compare numerically, and the sort can use the full path, dirname, or basename as its key. Unique sorting retains the first member of a run with the same key. When the key is basename, that can intentionally discard different full paths with matching final names.

The release also simplifies existing syntax

fish 3.5 allows scripts to catch SIGINT and SIGTERM through signal handlers or trap. It also removes support for caret-based standard-error redirection after its earlier deprecation. The notes identify 2> as the supported replacement, which already expresses the same destination stream.

I prefer these changes when their effects remain specific. The new builtin provides a vocabulary for path transformations and checks, while removing the old redirection spelling reduces special treatment of an otherwise ordinary character. A script can express a filesystem predicate, preserve the matching names, and pass them to another operation without mixing those decisions into one text-processing expression.