1. Overview
Scripting and programming frequently involve the task of comparing strings, which also applies to the Bash shell. In certain scenarios like when validating user names or comparing email addresses, performing case-insensitive string comparison becomes necessary.
In this tutorial, we’ll explore various techniques and approaches within Bash that enable case-insensitive string comparison.
2. Sample Task
For example, let’s suppose we wish to check whether a certain word is a case-insensitive palindrome. That is, we’d like to test if a word reads the same forwards and backwards regardless of letter case.
In Bash, we can use the rev command to reverse the characters of a string. This can help us check for palindromes:
$ string='Level'
$ [[ "$(rev <<< "$string")" == "$string" ]] && echo 'Palindrome' || echo 'Not a palindrome'
Not a palindrome
In this approach, we employ a here-string (<<<) to pass the string variable as input to rev within a subshell. Then, we use [[ ]] to test whether the string equates to its reverse version. The logical operators && and || ensure that if the two strings match, the word Palindrome is displayed in the shell; otherwise Not a palindrome is printed.
The test shows that the word Level isn’t a palindrome due to the discrepancy between the capitalization of the first L letter and the lowercase final letter. To disregard the letter case, we’d have to modify the string comparison approach.
In Bash, we can perform a case-insensitive string comparison using various techniques. Let’s delve into some of them.
3. Using [[ ]] and the =~ Operator
We can use the =~ operator within the [[ ]] construct to perform regular expression (regex) matching when testing for string comparison:
$ [[ "$(rev <<< "$string")" =~ ^[Ll][Ee][Vv][Ee][Ll]$ ]] && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
In this case, the regex expression encompasses the letters in Level. However, we use character classes to account for both lowercase and uppercase variations of each letter. This comprehensive approach allows us to account for various cases, such as Level, LeVeL, and so on.
The regex expression employed includes a caret (^) symbol denoting the start of a line and a dollar ($) symbol representing the end of a line. Essentially, we’re testing whether the reversed string is an exact match for the word level, disregarding the letter case.
However, listing both uppercase and lowercase versions of each character becomes tedious if the string is lengthy. Therefore, let’s explore some alternative methods.
4. Using the nocasematch Option
Another approach to case-insensitive comparisons is to enable the nocasematch option within the shell.
First, let’s examine the current option value using the shopt command:
$ shopt nocasematch
nocasematch off
By default, the nocasematch option is disabled, indicating that case sensitivity is in effect. Nonetheless, we can activate this option using the -s flag:
$ shopt -s nocasematch
$ [[ "$(rev <<< "$string")" == "$string" ]] && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
$ shopt -u nocasematch
To conduct a case-insensitive string comparison, we follow these steps:
- enable the nocasematch option
- perform the string comparison test using the [[ ]] construct
- disable the nocasematch option using the -u flag
In the comparison test, we check if the reversed string matches the original string, disregarding letter case. This is because letter case is ignored when the nocasematch option is enabled.
5. Normalizing to Lowercase (or Uppercase)
We can also use an approach that doesn’t require changing any shell options. The main idea is to convert both strings to either lowercase or uppercase before performing the comparison. This way, we ensure that the letter case is identical.
5.1. Using tr
One way to convert strings to lowercase (or uppercase) is via the tr command:
$ lowercase_reverse="$(echo "$string" | rev | tr '[:upper:]' '[:lower:]')"
$ lowercase_original="$(echo "$string" | tr '[:upper:]' '[:lower:]')"
$ [[ "$lowercase_reverse" == "$lowercase_original" ]] && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
Here, we first reverse the string using the rev command and then convert any uppercase characters to lowercase using tr. The character classes [:upper:] and [:lower:] are used to represent uppercase and lowercase characters, respectively.
Subsequently, in the string comparison test, we compare the reversed and lowercase version of the string with the lowercase version of the original.
Alternatively, we could’ve made the comparison in an all-uppercase context:
$ uppercase_reverse=$(echo "$string" | rev | tr '[:lower:]' '[:upper:]')
$ uppercase_original=$(echo "$string" | tr '[:lower:]' '[:upper:]')
$ [[ "$uppercase_reverse" == "$uppercase_original" ]] && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
The modifications involve exchanging the positions of [:lower:] and [:upper:] in the tr command. Additionally, the comparison within the [[ ]] construct should be made between the uppercase version of the reversed string and the uppercase version of the original string.
5.2. Using Built-in Parameter Expansion
We can also convert a string to either lowercase or uppercase via Bash’s built-in parameter expansion features:
$ echo "${string,,}"
level
$ echo "${string^^}"
LEVEL
During shell expansion, the ${string,} syntax can be used to convert the first character of a string to lowercase, while ${string,,} converts all characters to lowercase. Conversely, ${string^} converts the first character to uppercase, and ${string^^} converts all characters to uppercase.
With these parameter expansion features, the string comparison test becomes straightforward:
$ [[ "$(rev <<< "${string,,}")" == "${string,,}" ]] && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
Here, we compare the string to its reversed version after performing a lowercase conversion.
Alternatively, we can perform the test in uppercase mode:
$ [[ "$(rev <<< "${string^^}")" == "${string^^}" ]] && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
In this test, we convert the string to uppercase before comparing it to its reversed version.
6. Using grep
An alternative approach to case-insensitive string comparison is to use the -i flag with the grep command:
$ echo "$string" | rev | grep -qi "$string" && echo 'Palindrome' || echo 'Not a palindrome'
Palindrome
grep -i performs a case-insensitive search for the original string within the reversed string, both of which are of the same length. We use the -q flag to suppress any output from grep, while the logical operators test the exit code to know the result of the comparison.
7. Using awk
GNU awk is a powerful text processing tool that we can use here to perform the required task.
We can use the tolower() function in awk to convert a string to lowercase before conducting the string comparison:
$ echo "$string" | rev | awk -v x="${string,,}" '{if (tolower($0) == x) print "Palindrome"; else print "Not a palindrome";}'
Palindrome
In this case, we’re comparing the lowercase version of the reversed string to variable x, which holds the lowercase version of the original string. The -v option enables us to set the x variable within awk.
Another method involves enabling the IGNORECASE option within awk before performing string comparison:
$ echo "$string" | rev | awk -v x="$string" 'BEGIN{IGNORECASE=1} $0 == x { print "Palindrome" } $0 != x { print "Not a palindrome" }'
Palindrome
We set the IGNORECASE variable to a non-zero value within the BEGIN block to enable case-insensitive matching. If a case-insensitive match is found, the word Palindrome is printed; otherwise the phrase Not a palindrome is displayed.
8. Conclusion
In this article, we’ve explored several approaches for performing a case-insensitive string comparison in Bash. Specifically, we’ve examined using the =~ operator within the [[ ]] construct, leveraging the nocasematch shell option, converting strings to lowercase or uppercase before comparison, and employing tools like grep and awk.