Check if a String Contains a Substring in Bash - Quick Guide
Fast, idiomatic ways to check if a string contains a substring in Bash. Use [[ *substring* ]], case, or =~ for regex. Also covers quoting and portability.
How to check if a string contains a substring in Bash
I have a string in Bash:
string="My string"
How can I test if it contains another string? I tried using:
if [ $string ?? 'foo' ]; then
echo "It's there!"
fi
Where ?? is my unknown operator. Is using echo and grep the correct approach?
if echo "$string" | grep 'foo'; then
echo "It's there!"
fi
That seems a bit clumsy. What’s the proper way to check for substrings in Bash?
To do a bash check substring (test whether a bash string contains a substring), use Bash’s built-in pattern matching: [[ "$string" == *"$substring"* ]] or a case statement — both avoid forking grep. Use [[ $string =~ regex ]] for real regular-expression matching (captures go to BASH_REMATCH); grep -q or awk work for portability but are slower and spawn external processes.
Contents
- Quick examples (bash substring, bash string contains)
- Why [[ ]] / case are better than echo | grep
- Regex matching with =~ and capturing
- Edge cases and pitfalls (quoting, special characters, empty substring)
- Case-insensitive checks and POSIX portability
- Performance and best practices (bash check substring)
- Sources
- Conclusion
Quick examples (bash substring, bash string contains)
If you have:
string="My string"
substring="foo"
The simplest, most idiomatic Bash check is:
if [[ "$string" == *"$substring"* ]]; then
echo "It's there!"
fi
[[ ... ]]is a Bash conditional that understands pattern matching.*"$substring"*is a glob pattern: “anything, then the literal substring, then anything.”
POSIX-portable alternative (works in plain /bin/sh):
case "$string" in
*"$substring"*) echo "It's there!";;
*) echo "Not found";;
esac
If you prefer an external-tool approach (portable but slower), use grep -qF (fixed-string, quiet):
# portable-ish, but forks grep
if printf '%s\n' "$string" | grep -qF -- "$substring"; then
echo "It's there!"
fi
# or with bash's here-string (bash-only)
if grep -qF -- "$substring" <<< "$string"; then
echo "It's there!"
fi
For quick reusable checks, a small function:
contains() {
case "$1" in
*"$2"*) return 0 ;;
*) return 1 ;;
esac
}
if contains "$string" "foo"; then
echo "found"
fi
For more examples and explanation of pattern matching, see the guide at https://docs.vultr.com/how-to-check-for-a-substring-in-bash and a practical how‑to at https://www.delftstack.com/howto/linux/string-contains-a-substring-in-bash/.
Why [[ ]] / case are better than echo | grep
Why not just echo "$string" | grep 'foo' every time? Two reasons:
- Performance:
[[ ... ]]andcaseare shell builtins — no new process is spawned. In tight loops or scripts that run often, avoidinggrepsaves CPU and latency. See a practical test and discussion at https://www.howtogeek.com/i-found-using-grep-or-sed-in-bash-scripts-is-painfully-slow-but-heres-how-i-fixed-it/. - Simplicity and safety: builtin patterns avoid quoting pitfalls and are easier to read.
grepinterprets its pattern as a regex by default; use-Fto treat it as a fixed string. Also,echocan mangle some inputs; preferprintf '%s\n'if you pipe to external tools.
Use [[ "$string" == *"$substring"* ]] for literal substring checks in Bash, and case for scripts that must run under /bin/sh.
Regex matching with =~ and capturing
If you need regular expressions (not simple substring), use =~:
string="version 1.2.3"
if [[ $string =~ ([0-9]+.[0-9]+.[0-9]+) ]]; then
echo "Version: ${BASH_REMATCH[1]}"
fi
Notes:
- The right-hand side is an extended regular expression. Do NOT quote the regex; quoting makes it a literal string and prevents regex parsing.
- Matches are stored in the array
BASH_REMATCH([0]is the whole match,[1]the first capture group, …).
If your regex comes from user input (you want a literal match but are using =~), escape regex meta-characters first:
escape_regex() {
printf '%s' "$1" | sed -e 's/[][\/.^$*+?{}()|]/\\&/g'
}
needle_regex=$(escape_regex "$substring")
if [[ $string =~ $needle_regex ]]; then
echo "Found (literal match via =~)"
fi
(That sed expression escapes characters that are special in POSIX ERE.)
For additional examples and parameter-expansion tricks, see https://www.delftstack.com/howto/linux/string-contains-a-substring-in-bash/ and https://www.namehero.com/blog/bash-string-comparison-the-comprehensive-guide/.
Edge cases and pitfalls (quoting, special characters, empty substring)
- Quoting: In
[[ ]]word-splitting and pathname expansion don’t happen, but you should still write*"$substring"*(quote the variable) so a substring containing spaces or glob characters is treated safely. - Single-bracket
[ ... ](thetestbuiltin) is NOT reliable for pattern matching. Prefer[[ ... ]]orcasefor substring checks. - Empty needle:
substring=""—[[ "$string" == *""* ]]returns true. If you don’t want that, check for an empty needle first ([ -z "$substring" ] && return 1). - Meta-characters: If
$substringcontains*,?,[etc., quoting inside the pattern (*"$substring"*) prevents those characters from being treated as glob operators; with=~you must escape regex metas. - Newlines / binary data: pattern matching works on the variable’s contents; if dealing with multi-line or binary blobs, external tools may behave differently.
- Portability:
[[and${var,,}(lowercasing) require Bash. Usecaseandtrfor POSIX portability.
Case-insensitive checks and POSIX portability
Want case-insensitive substring checks?
- Bash-native (fast):
shopt -s nocasematch
if [[ "$string" == *"$substring"* ]]; then
echo "found (case-insensitive)"
fi
shopt -u nocasematch
- Bash 4+ lowercasing:
if [[ "${string,,}" == *"${substring,,}"* ]]; then
echo found
fi
- POSIX (portable) approach using
tr+case:
lc_string=$(printf '%s' "$string" | tr '[:upper:]' '[:lower:]')
lc_sub=$(printf '%s' "$substring" | tr '[:upper:]' '[:lower:]')
case "$lc_string" in
*"$lc_sub"*) echo "found";;
esac
Use the approach that matches your script’s portability requirements.
Performance and best practices (bash check substring)
- For literal substring checks in Bash: prefer
[[ "$string" == *"$substring"* ]]— fast and readable. - For shell portability: use
case. - For regex needs: use
[[ $string =~ regex ]]andBASH_REMATCHfor captures; escape user input before inserting into a regex. - Use
grep -qF --when you must rely on external tools or search files; addLC_ALL=Cfor faster ASCII-only searches when appropriate. - Always quote variable expansions when they might contain spaces or special characters.
- Encapsulate repeated logic in a small function (see
contains()above); it makes tests clearer and centralizes escaping/normalization.
Sources
- https://docs.vultr.com/how-to-check-for-a-substring-in-bash
- https://www.delftstack.com/howto/linux/string-contains-a-substring-in-bash/
- https://www.howtogeek.com/i-found-using-grep-or-sed-in-bash-scripts-is-painfully-slow-but-heres-how-i-fixed-it/
- https://www.namehero.com/blog/bash-string-comparison-the-comprehensive-guide/
Conclusion
For checking whether a string contains a substring in Bash, prefer built-ins: [[ "$string" == *"$substring"* ]] (or case) for simple literal checks and [[ $string =~ regex ]] for regex matches. These approaches are cleaner and faster than piping to grep in most scripts; use grep -qF when portability or file-searching requires an external tool. In short: use Bash pattern matching for typical bash substring checks, reserve grep for situations where you specifically need its features.