Case-insensitive substring search in a shell script

You can do case-insensitive substring matching natively in bash using the regex operator =~ if you set the nocasematch shell option. For example

s1="hElLo WoRlD"
s2="LO"

shopt -s nocasematch

[[ $s1 =~ $s2 ]] && echo "match" || echo "no match"
match

s1="gOoDbYe WoRlD"
[[ $s1 =~ $s2 ]] && echo "match" || echo "no match"
no match

First here's a simple example script that doesn't ignore case:

#!/bin/bash
if [ $(echo hello) == hello ]; then
    echo it works
fi

Try changing the string hello on the right, and it should no longer echo it works. Try replacing echo hello with a command of your choosing. If you want to ignore case, and neither string contains a line break, then you could use grep:

#!/bin/bash
if echo Hello | grep -iqF hello; then
    echo it works
fi

The key here is that you are piping a command output to grep. The if statement tests the exit status of the rightmost command in a pipeline - in this case grep. Grep exits with success if and only if it finds a match.

The -i option of grep says to ignore case.
The -q option says to not emit output and exit after the first match.
The -F option says to treat the argument as a string rather than a regular expression.

Note that the first example uses [ expression ] which allows direct comparisons and various useful operators. The second form just execs commands and tests their exit status.


For a case-sensitive string search of the value of the variable needle in the value of the variable haystack:

case "$haystack" in
  *"$needle"*) echo "present";
  *) echo "absent";
esac

For a case-insensitive string search, convert both to the same case.

uc_needle=$(printf %s "$needle" | tr '[:lower:]' '[:upper:]' ; echo .); uc_needle=${uc_needle%.}
uc_haystack=$(printf %s "$haystack" | tr '[:lower:]' '[:upper:]' ; echo .); uc_haystack=${uc_haystack%.}
case "$uc_haystack" in
  *"$uc_needle"*) echo "present";;
  *) echo "absent";;
esac

Note that the tr in GNU coreutils doesn't support multibyte locales (e.g. UTF-8). To make work with multibyte locales, use awk instead. If you're going to use awk, you can make it do the string comparison and not just the conversion.

if awk 'BEGIN {exit !index(toupper(ARGV[2]), toupper(ARGV[1]))}' "$needle" "$haystack"; then
  echo "present"
else
  echo "absent"
fi

The tr from BusyBox doesn't support the [:CLASS:] syntax; you can use tr a-z A-Z instead. BusyBox doesn't support non-ASCII locales.

In bash (but not sh), version 4.0+, there is a built-in syntax for case conversion, and a simpler syntax for string matching.

if [[ "${haystack^^}" = *"${needle^^}"* ]]; then
  echo "present"
else
  echo "absent"
esac