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

Fish string: Match, Replace, Escape, and Preserve Text Boundaries

Use Fish's string builtin with deliberate glob or regex modes, literal replacement, stream boundaries, and output semantics that scripts can test.

Fish’s string builtin handles transformations that shell scripts often delegate to a chain of grep, sed, cut, and tr processes. It can match, replace, split, join, trim, escape, and collect text while returning an exit status that can be used in control flow. These operations are not interchangeable: string match defaults to whole-string glob matching, string replace defaults to a literal search, and regex modes use their own documented behavior.

The useful design principle is to decide whether a value is one string, a list of strings, or a stream of newline-delimited records. string accepts arguments or standard input, but combining both sources is an error for its subcommands. A command substitution may further turn output lines into list elements. Make those boundaries explicit before building filenames, patterns, or command arguments.

Choose glob matching or regular expressions explicitly

By default, string match uses a glob pattern and tests whether the pattern matches the entire input string. This is different from a regular-expression search that can match a substring. Use –regex only when regular-expression semantics are intended, and add –entire when the full input string should be returned rather than only the matching portion.

function validate_identifier --argument-names identifier
    if string match --quiet --regex '^service-[0-9]+$' -- $identifier
        printf 'accepted: %s\n' $identifier
    else
        printf '%s\n' 'identifier must be service-N' >&2
        return 2
    end
end

validate_identifier 'service-42'

The regular expression is quoted so Fish passes it as a single argument. – ends option processing before an input string that might begin with a dash. A match status can control an if; do not rely on printed output alone to identify success. With –all, the command can emit multiple matches. With –index, it reports positions and lengths rather than text, and named capture groups can assign Fish variables according to the builtin’s documented scope behavior. Those are different return formats and should have separate tests.

Treat a regex as a small language with its own metacharacters. Quoting the Fish word preserves the pattern as one argument but does not make regex punctuation literal. If matching user-provided text literally, use glob/literal comparison or escape that value for regex using the documented string escape –style=regex behavior. Escaping for regex is not the same as shell escaping and is not a general sanitizer for another tool.

Use replace’s literal default for ordinary data

string replace treats the pattern as literal text unless –regex is supplied. That makes it a good default for replacing a known delimiter or marker without exposing a regex grammar:

set -l normalized (string replace --all '/' '_' -- $input_path)
printf '%s\n' $normalized

The replacement is non-overlapping. Without –all, only the first applicable match is replaced; with –filter, strings are printed only when a replacement occurs. The exit status reports whether a replacement took place, so a substitution that does not find the input text may return nonzero. If nothing to replace is normal, put the operation in a conditional or use a deliberate quiet mode rather than allowing strict error handling to misclassify it.

When –regex is enabled, the pattern is a PCRE regular expression and replacement strings can refer to capturing groups using the syntax documented by Fish. That is useful for structured normalization, but it also increases the number of interpretation layers. Keep the regex static when possible, test edge cases, and validate any data that later becomes a path, option, or program identifier. A successful textual replacement does not prove that the resulting value is safe for its next consumer.

string match –quiet and string replace –quiet are useful when only the status matters. Quiet mode can stop reading input once the result is known, which is helpful for a large stream when early exit is acceptable. It is not appropriate if a caller expects every line to be processed or if upstream resources require the stream to be drained. Choose an explicit consumer contract rather than adding –quiet solely to reduce output.

Understand standard input and argument input

String subcommands generally take text from their command-line arguments when arguments are supplied, or from standard input when it is connected to a pipe or file. Supplying both command-line strings and piped input is an error. This is useful because it makes the input source unambiguous, but it means a refactor from a direct invocation to a pipeline can change behavior if old arguments remain on the command line.

printf '%s\n' 'alpha' 'beta' | string upper
string upper -- 'alpha' 'beta'

The pipeline form processes input records from standard input; the argument form processes each argument. Do not assume a newline inside one argument becomes several input records. Conversely, stdin-oriented processing uses a line format, so an embedded newline is a record delimiter unless the tool or a separate NUL-safe protocol says otherwise.

string collect can combine a stream into one command-substitution element. By default, it trims trailing newlines; –no-trim-newlines retains them, and –allow-empty can preserve an empty result as one output element. Those flags matter when the exact file bytes or a final newline is part of the interface. For large input, avoid collecting an unbounded stream into a shell variable; transform it incrementally or write it to a controlled file.

Never use a string operation to fake an argv serializer. Replacing spaces with backslash-space, joining arguments with a delimiter that may appear in the data, or emitting script-style escapes does not preserve original argument boundaries unless the receiving parser implements the exact same format. Keep Fish lists as lists and use string for text transformation, not as a conversion into shell source.

Escape for a named output format only

string escape has several styles. The default script style produces text documented as usable in Fish eval to reconstruct the original argument. Other styles can encode variable names or URL components, and regex style is for literal matching in a regular expression. None of these modes is a universal make-safe operation. The correct escape depends on the next parser and the grammar it expects.

set -l original 'two words; $HOME'
set -l regex_literal (string escape --style=regex -- $original)
string match --quiet --regex -- $regex_literal $original

This example illustrates escaping an input for literal use in a regular-expression pattern. If the pattern is later inserted into a Fish command, a URL, JSON, or a SQL expression, regex escaping no longer answers whether that value is safe. Prefer an API that accepts data separately from its format string. If a documented serialization is required, round-trip it with the matching parser and include version compatibility tests.

Use – when a user-supplied string could begin with a dash, and quote each Fish list element where a command expects one argument. Fish’s list expansion is different from Bash word splitting; a string result can produce zero, one, or multiple elements depending on how command substitution and quoting are used. Check count or print each element with delimiters in a test rather than inferring cardinality from a pretty terminal display.

Make transformations testable and predictable

Put compound text rules in small functions with explicit input and output. Test no-match, one match, multiple matches, empty input, embedded whitespace, regex metacharacters, Unicode text, and a value beginning with a dash. Include newlines where the consumer permits them. If output must preserve trailing newlines, make that requirement explicit and test string collect -N or an alternative stream protocol.

For code that only needs a boolean, use a quiet matching mode in an if and keep diagnostics separate. For code that needs transformed output, capture it only if its size is bounded. Shell command substitutions are convenient for short identifiers but can be poor containers for large or binary streams. Fish variables are text/list values; they are not arbitrary byte arrays.

When replacing an external pipeline with string, compare semantics before claiming it is equivalent. grep generally reports matching lines, sed has explicit pattern-space and file behavior, and cut has delimiter/field conventions. Fish’s string commands take strings, not filenames; redirect or stream file contents explicitly. The builtin can reduce subprocesses and make quoting clearer, but only when the transformation’s grammar and exit status match the original requirement.

Finally, keep the mode visible in code. A reviewer should be able to tell whether a pattern is a glob or regex, whether replacement is literal or regex-based, whether the input is arguments or stdin, and whether a missing match is success or failure. Those choices turn string from a shorthand into a predictable text-processing interface.

Keep the Fish version in test output when scripts depend on newer subcommands or flags. Documentation for a current release may describe features that are absent from an older package in a long-lived operating system image. A simple feature probe can fail with an actionable message before a deployment script starts mutating files, while a test matrix can verify the oldest supported release and the current release independently.

Related:

Sources:

Comments