23

I want to know which method is better to check if a var (input by user on keyboard) matches with a regex in a case insensitive way. I know there are some different possibilities. Example: I want a regex matching an empty value and all of this list: Y, N, y, n, Yes, No, YES, NO

I searched looking different methods. Not sure if could be another better. I'll put a couple of them working for me.

  • First one is a little "tricky" setting all to uppercase for the comparison:

    #!/bin/bash
    yesno="null" #any different value for initialization is valid
    while [[ ! ${yesno^^} =~ ^[YN]$|^YES$|^NO$|^$ ]]; do
        read -r yesno
    done
    
  • Second one is using shopt -s nocasematch. But not sure if after doing that it can be reverted because I don't want to set this for all the script.

    #!/bin/bash
    yesno="null" #any different value for initialization is valid
    shopt -s nocasematch
    while [[ ! ${yesno} =~ ^[yn]$|^yes$|^no$|^$ ]]; do
        read -r yesno
    done
    

Can these regex get improved in any way? Is there a better (more elegant) method? On second method, is there a way to revert that setting?

codeforester
  • 39,467
  • 16
  • 112
  • 140
OscarAkaElvis
  • 5,384
  • 4
  • 27
  • 51
  • 1
    `declare -l yesno; yesno="YES"; echo "$yesno"`? – Cyrus May 22 '17 at 07:24
  • 4
    To revert `shopt -s nocasematch`: `shopt -u nocasematch`. See: `help shopt` – Cyrus May 22 '17 at 07:34
  • 1
    Since you asked about the regex, you don't need to repeat the line anchors or the Y/N in Yes/No. This works: `[[ ! ${yesno^^} =~ ^(Y(ES)?|NO?)$ ]]` and is probably more efficient. It might be just me, but I find `unset yesno` more clear in your example than setting it to a value that forces an initial false condition (`yesno="null"`), especially since the condition is negated (while NOT true). But that's probably more a matter of style than good practice. – Luv2code Jan 07 '19 at 22:41
  • BTW since `^$` is not Y/N/YES/NO it's not necessary to specify it. If you had a situation where null value was a valid response, you could use the zero-or-one quantifier `?` around the outer parens, e.g.: `[[ ${respons,,} =~ ^(bye|quit|exit)?$ ]] && exit 0` – Luv2code Jan 07 '19 at 22:56

3 Answers3

33

You can first convert the string into lowercase and check it. Then you don't need to touch nocasematch at all. The content of the variable is left unmodified as well.

#
# NOTE: This requires Bash 4.0+ (bash 4.0 was released on 2009-02-20)
#
# use the ${var,,} syntax to convert to lowercase
#
while [[ ! ${yesno,,} =~ ^(y|n|yes|no)$ ]]; do
    read -r -p "yes/no? " yesno
done
pynexj
  • 19,215
  • 5
  • 38
  • 56
  • 6
    This looks more elegant than the `shopt` way. – codeforester Sep 13 '18 at 21:51
  • 4
    You should probably add that this is not posix compliant and requires bash >= 4.0.0 – Dave Apr 12 '19 at 01:20
  • 2
    You may need word boundary and any character checks to get this to work if you need to check within multi-word user input. I was struggling for ages because it would give false results for things like `ye s`. Something like this maybe needed. `^.*\b(y|n|yes|no)\b.*$` – Aaron Ford Jan 21 '22 at 07:33
18

shopt is good approach as you are able to retain originally entered value in variable yesno.

You can just refactor your regex a bit:

#!/bin/bash

yesno="null"

# set nocasematch option
shopt -s nocasematch

while [[ ! ${yesno} =~ ^([yn]|yes|no)?$ ]]; do
    read -r -p "Enter a yes/no value: " yesno
done

# unset nocasematch option
shopt -u nocasematch

# examine your variable
declare -p yesno
anubhava
  • 761,203
  • 64
  • 569
  • 643
4

The following variant with parenthesis has the advantage of leaving the case sensitivity attribute of the current shell intact by introducing temporary shell context:

if ( 
  shopt -s nocasematch; 
  [[ "${yesno}" =~ ^[yn]$|^yes$|^no$|^$ ]]
  ); then
  echo "Yes"
else
  echo "No"
fi
minus one
  • 642
  • 1
  • 7
  • 28