0

I'm new to bash, and the below while loop is driving me crazy. I want the loop to exit when the user enters y or n but it fails to do so. I have tried different ways but no luck. Anyone can point me in the right direction?

    echo "want to go for a walk Y/N "
    read answer
    while [ "$answer" != "y" ]  ||  [ "$answer" != "n" ]  ; do 
               
        echo "Enter y or n"
        read answer
    done

The second solution also is in the same scenario

    echo "want to go for a walk Y/N "
    read answer
    while [ "$answer" != "y"   ||   "$answer" != "n" ]  ; do 
               
        echo "Enter y or n"
        read answer
    done
kfkhalili
  • 996
  • 1
  • 11
  • 24
  • 1
    Please add a shebang and then paste your script at http://www.shellcheck.net/ and try to implement the recommendations made there. – Cyrus Aug 29 '20 at 06:53
  • 2
    Does this answer your question? [How do I prompt for Yes/No/Cancel input in a Linux shell script?](https://stackoverflow.com/questions/226703/how-do-i-prompt-for-yes-no-cancel-input-in-a-linux-shell-script) – Léa Gris Aug 29 '20 at 08:05

2 Answers2

2

Of course it won't exit. That loop never will.

In your code, answer is always NOT 'y' OR NOT 'n' (together, at the same time).

It is so because if you select 'y' then it's NOT 'n' and if you select 'n' then it's NOT 'y'.

If you have TRUE || FALSE = TRUE And if you have FALSE || TRUE = TRUE, so the loop keeps going.

The condition you want to use is && (AND), so while answer is NOT 'y' AND NOT 'n' then go on, but if it is one of them then exit.

TRUE && FALSE => FALSE and FALSE && TRUE => FALSE and that's what you need for the loop to finally end.

#!/bin/bash

echo "want to go for a walk y/n "
read -r answer
while [ "$answer" != "y" ]  &&  [ "$answer" != "n" ]  ; do 
           
    echo "Enter y or n"
    read -r answer
done

The -r flag in read -r var is to avoid reading backslash as an escape character. It is not relevant for this toy problem but it is a best practice to include it.

Comment from David: Keep in mind that the code doesn't handle case, so Y is not recognized as y.

kfkhalili
  • 996
  • 1
  • 11
  • 24
  • 1
    You will also never match `'Y'` or `'N'` checking for `'y'` and `'n'` `:)` – David C. Rankin Aug 29 '20 at 07:09
  • But of course! Good catch! I made it more explicit. – kfkhalili Aug 29 '20 at 07:12
  • Other alternatives would be: `while ! [ "$answer" = "y" ] || [ "$answer" = "n" ]` or `until [ "$answer" = "y" ] || [ "$answer" = "n" ]` – Cyrus Aug 29 '20 at 07:15
  • Correct. I tried to change as little as possible to make it easier to understand. – kfkhalili Aug 29 '20 at 07:17
  • Or something truly ugly: `printf "go for a walk (y/n)? "; while read -r answer && [ "$(tr '[:upper:]' '[:lower:]' <<< "${answer:0:1}")" != 'y' -a "$(tr '[:upper:]' '[:lower:]' <<< "${answer:0:1}")" != 'n' ]; do printf "go for a walk (y/n)? "; done; echo "answer: $answer"` which handles case conversion for you as well `:)` – David C. Rankin Aug 29 '20 at 07:49
  • `until [[ ${answer::1} =~ [YyNn] ]] ; do ...` will match any input that starts with [YyNn]. If we want only single char answer condition, will be `[[ #${#answer} = 1 && $answer =~ [YyNn] ]]` – Yuri Ginsburg Aug 29 '20 at 09:09
  • @YuriGinsburg `[[ #${#answer} = 1 && $answer =~ [YyNn] ]]` is not correct. Should be `[[ ${#answer} -eq 1 && $answer =~ [YyNn] ]]` and may be simplified to `[[ $answer =~ ^[YyNn]$ ]]` – Léa Gris Aug 29 '20 at 10:47
  • 1
    You can convert the answer to lowercase inside your check with `${answer,,}` – Walter A Aug 29 '20 at 13:40
  • 1
    @WalterA it's worth noting that the syntax `${answer,,}` is Bash 4.0 and can be subject to portability issues. – kfkhalili Aug 31 '20 at 11:41
0

Don't forget in a while loop, the first commands list may also contain multiple statements:

#!/usr/bin/env sh

echo "want to go for a walk y/n "

while
  read -r answer
  case $answer in
    [yYnN]) false ;;
  esac
do
  echo "Enter y or n"
done

Or using until and Bash's Extended Regular Expression:

#!/usr/bin/env bash

echo "want to go for a walk y/n "

until
  read -r answer
  [[ $answer =~ [yYnN] ]]
do
  echo "Enter y or n"
done

And a featured, case-insensitive POSIX compatible implementation:

#!/usr/bin/env sh

echo "want to go for a walk y/n "

until
  read -r answer
  case $answer in
    [yY])
      echo "Handling Yes answer stuffs"
      ;;
    [nN])
      echo "Stuffs for No answer"
      ;;
    *)      # Landing here if invalid answer
      false # Proceed with the do...done statements
      ;;
  esac
do
  echo "Enter y or n"
done
Léa Gris
  • 17,497
  • 4
  • 32
  • 41
  • great set of answers. I'd use `while true; do ... break; done` for (arguably) more robust alternative – tomi Aug 29 '20 at 20:28