← Blog
Linux & Server Operations

Choose the right Bash substring test: literal, glob, or regex

Avoid quoting traps and needless subprocesses by matching shell strings with deliberate Bash or portable POSIX semantics.

By Emmanuel Corels

A substring test looks trivial until the needle contains spaces, *, a bracket, or data supplied by another system. The safe choice depends on whether the needle is literal text, a shell pattern, or a regular expression—and whether the script actually runs in Bash.

For a literal needle in Bash

haystack='deployment state: ready for traffic'
needle='ready for traffic'

if [[ $haystack == *"$needle"* ]]; then
    printf '%s\n' 'match'
fi

Inside [[ ... ]], the right side of == is a pattern. The two unquoted * characters mean “any string,” while quoting "$needle" makes characters from the variable literal. This is the accepted solution’s important boundary: quoting the whole expression as "*$needle*" would also quote the wildcards and test equality with literal asterisks.

[[ is Bash syntax, not the portable [/test command. A file beginning with #!/bin/sh must not rely on it, and invoking such a script with sh script ignores a Bash shebang.

Use case for a portable shell

case $haystack in
    *"$needle"*) printf '%s\n' 'match' ;;
    *)           printf '%s\n' 'no match' ;;
esac

POSIX case performs shell pattern matching without spawning grep. The same quoting boundary keeps the caller-provided needle literal. This works in shells that implement POSIX syntax and is generally the best answer for a genuinely portable script.

When the pattern is intentional

If operators define a glob, leave that pattern variable unquoted in the pattern position and document that it is code-like input:

pattern='release-+([0-9]).log'
shopt -s extglob
if [[ $filename == $pattern ]]; then
    printf '%s\n' 'approved name shape'
fi

Extended globs require extglob, and parsing timing matters for constructs such as functions. Do not accept an untrusted “literal substring” and silently interpret it as a glob; *, ?, and brackets change meaning.

Regex is a separate contract

regex='^release-[0-9]+[.]log$'
if [[ $filename =~ $regex ]]; then
    printf '%s\n' 'regex match'
fi

=~ uses POSIX extended regular expressions and can populate BASH_REMATCH. Quoting the entire regex variable forces literal matching in current Bash, which is a frequent migration bug. Store the regex in a variable to make shell quoting easier to review. Regex matching is appropriate for structure and captures, not merely because a literal needle happens to work today.

External tools have different trade-offs

grep -F treats the needle as a fixed string and is useful when filtering streams or files:

if printf '%s\n' "$haystack" | grep --fixed-strings --quiet -- "$needle"; then
    printf '%s\n' 'match'
fi

The -- prevents a needle beginning with - from becoming an option. For one in-memory value this adds processes and a pipeline; in a loop that can be expensive. It also introduces locale, newline, and binary-data considerations. Shell variables cannot contain NUL bytes at all, so do not use shell substring tests as a binary protocol parser.

Diagnosis checklist

  1. Confirm the interpreter with the shebang and the actual launch command.
  2. Print shell-escaped values in an authorized test using printf '%q\n'; never expose secrets merely to debug quoting.
  3. Classify the requirement as literal, glob, or regex before selecting syntax.
  4. Test an empty needle. Mathematically it matches every string; decide whether the application should reject it instead.
  5. Test spaces, leading hyphens, literal * ? [ ], newlines, non-ASCII text, and a non-match.
  6. Run bash -n and a shell linter, then exercise the supported Bash versions.

Failure modes and recovery

An over-broad match in a cleanup or deployment script can select the wrong files, services, or hosts. Make discovery and mutation separate phases: print or persist the proposed set, apply allowlists and count limits, then mutate. Use -- with utilities, quote normal expansions, and avoid eval.

If a bad matcher has already run, stop the automation and preserve logs. Restore files from a verified backup, revert configuration through the source-controlled deployment path, and reconcile partial changes idempotently. Fixing the predicate prevents recurrence but does not reverse deletions or remote actions. Roll out the corrected matcher first in report-only mode and compare its selection against a known fixture corpus.

Attribution and current verification

This guide was prompted by davidsheldon’s Stack Overflow question and Adam Bellaire’s accepted answer, used under CC BY-SA. Current behavior was independently checked against the GNU Bash manual for [[, case, and regex conditionals, conditional expression operators, and shell pattern matching.

Primary source: Review the official reference ↗