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.
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
- Confirm the interpreter with the shebang and the actual launch command.
- Print shell-escaped values in an authorized test using
printf '%q\n'; never expose secrets merely to debug quoting. - Classify the requirement as literal, glob, or regex before selecting syntax.
- Test an empty needle. Mathematically it matches every string; decide whether the application should reject it instead.
- Test spaces, leading hyphens, literal
* ? [ ], newlines, non-ASCII text, and a non-match. - Run
bash -nand 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 ↗