Myrddin Emrys
Myrddin Emrys

Reputation: 44005

How do I prompt for Yes/No/Cancel input in a Linux shell script?

I want to pause input in a shell script, and prompt the user for choices.
The standard Yes, No, or Cancel type question.
How do I accomplish this in a typical bash prompt?

Upvotes: 1907

Views: 1392327

Answers (30)

Toby Speight
Toby Speight

Reputation: 30933

Here's a Bash function that deals with simple yes/no questions (no "cancel"), which

  • is translated to the user's language (assumes the provided question is already localised) and accepts input in the same language, both with fallbacks to English
  • allows a default to be specified, which is presented in capital letters, according to established convention - or if the language doesn't have uppercase/lowercase distinction, then it's highlighted with [⋯]
  • returns success or failure so it can be used directly as a command in conditionals
  • satisfies Shellcheck.
#!/bin/bash

# Usage: y_or_n MESSAGE [0|1]
# First argument is the question to ask
# Second argument, if present, is 0 or 1 to default to yes or no, respectively
#
# * The "yes/no" prompt is translated to the user's language (the
#   question must be localised by the caller).
# * Input is accepted in the same language.
# * Language falls back to English if message catalogue is not available.
# * The question is repeated until a valid response is entered.
# * One reply may be specified as default if the user just gives an
#   empty string, which is presented in capital letters, as is the
#   convention - or if the language doesn't have uppercase/lowercase
#   distinction, then it's highlighted with [⋯].
# * If no default is specified, empty input is not valid.
# * It returns success or failure so it can be used directly as a command
#   in conditionals.
y_or_n() {
    local messages true false answer default
    readarray -t messages < <(locale LC_MESSAGES)
    true=${messages[2]:=yes}
    false=${messages[3]:=no}
    default=${2-}
    case "$default" in
        [01])
            local tf=(true false)
            local -n d=${tf[$default]}
            if [ "${d,,}" != "${d^^}" ]
            then
                # downcase the options
                true=${true,,}
                false=${false,,}
                # upcase the default
                d=${d^^}
            else
                # alt. highlight if upper case is same as lower
                d="[$d]"  # or "$d(*)" etc
            fi
            ;;
        '')
            # no default
            ;;
        *)
            # Invalid, so ignore, with a warning
            echo >&2 "Usage: ask_with_default message [0|1]"
            default=
            ;;
    esac

    while read -rp "$1 ($true/$false) " answer
    do if [[ "$answer" =~ ${messages[0]:=^[yY]} ]]
       then
           return 0
       elif [[ "$answer" =~ ${messages[1]:=^[nN]} ]]
       then
            return 1
       elif ! [ "$answer" ] && [ "$default" ]
       then
            return "$default"
       fi
    done
    # if we reach here, then read failed
    echo >2 "Failed to read input - aborting"
    exit 1
}

if y_or_n "Is this a good question?" 0
then
    echo "That makes me happy."
else
    echo "I'm sorry you don't like it."
fi

Example usage (I haven't bothered to translate the question here, just for clarity)

if ask_with_default "Is this good?" 0
then
    echo "That makes me happy."
else
    echo "I'm sorry you don't like it."
fi

Here's the yes/no selection in a few locales I have available here:

  • C: (YES/no)
  • cy: (IE/na)
  • gd: (THA/chan eil)
  • kl: (AAP/naagga)
  • el: (ΝΑΙ/όχι)
  • ru_UA: (ДА/нет)
  • ko: ([예]/아니요)
  • ja: ([はい]/いいえ)

Upvotes: 1

F. Hauri  - Give Up GitHub
F. Hauri - Give Up GitHub

Reputation: 70977

At least five answers for one generic question.

Depending on

  • compliant: could work on poor systems with generic environments
  • specific: using so called bashisms

and if you want

  • simple 'in line'' question / answer (generic solutions)
  • pretty formatted interfaces, like or more graphical using libgtk or libqt...
  • use powerful readline history capability

1. POSIX generic solutions

You could use the read command, followed by if ... then ... else:

#/bin/sh
printf 'Is this a good question (y/n)? '
read answer

if [ "$answer" != "${answer#[Yy]}" ] ;then 
    echo Yes
else
    echo No
fi

(Thanks to Adam Katz's comment: Replaced the test with the new one above that's more portable and avoids one fork :)

1.1 POSIX, but single key feature

But if you don't want the user to have to hit Return, you could write:

(Edited: As @JonathanLeffler rightly suggest, saving stty's configuration could be better than simply force them to sane.)

#/bin/sh
printf 'Is this a good question (y/n)? '
old_stty_cfg=$(stty -g)
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
if [ "$answer" != "${answer#[Yy]}" ];then
    echo Yes
else
    echo No
fi

1.2 POSIX, but single key feature localized

Using locale command, inspired by Léa Gris's idea for handling locales, but for this I just need *yes expression*:

#/bin/sh
yExpr=$(locale yesexpr) 
printf 'Is this a good question (y/n)? '
old_stty_cfg=$(stty -g)
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
if [ "$answer" != "${answer#${yExpr#^}}" ];then
    echo Yes
else
    echo No
fi

Note: This was tested under , , , and !

1.3 Same, but waiting explicitly for y, n or from locale

#/bin/sh
set -- $(locale LC_MESSAGES)
yExpr="$1"; nExpr="$2"
printf 'Is this a good question (y/n)? '
old_stty_cfg=$(stty -g)
stty raw -echo
answer=$( while ! head -c 1 | grep "$yExpr\|$nExpr" ;do true ;done )
stty $old_stty_cfg
if [ "$answer" != "${answer#${yExpr#^}}" ];then
    echo Yes
else
    echo No
fi

1.4 Ask for Y/N POSIX shell function with single key feature

If you plan to use this repetitively, you may want to create a dedicated function:

#/bin/sh
askFor() {
    __aF_yExpr="$(locale yesexpr)" __aF_nExpr="$(locale noexpr)"
    printf '%s? ' "$*"
    __aF_oStty=$(stty -g)
    stty raw -echo
    __aF_key=$( while ! head -c 1| grep "$__aF_yExpr\|$__aF_nExpr"; do :;done )
    stty $__aF_oStty
    if [ "$__aF_key" != "${__aF_key#${__aF_yExpr#^}}" ]; then echo Yes
    else echo No; return 1
    fi
}
verbEcho() { [ "$quietMode" -gt 0 ] || echo $*;}
askFor Enable verbose mode
quietMode=$?

verbEcho Ask for continuing
askFor Do you want to continue this demonstration || exit

toInstall=''
for package in moon-buggy pacman4console junior-games-text; do
    verbEcho Ask for Installation of $package
    if askFor Do I install full "$package"; then
        verbEcho "Add $package to list"
        toInstall="$toInstall $package"
    fi
done
if [ -z "$toInstall" ]; then
    echo Nothing to do.
elif askFor Do you really want to install $toInstall; then
    verbEcho Proceed installation of $toInstall
    echo sudo apt install $toInstall  # Drop `echo` for real installation
fi

1.4b Same, bu strictly answer SO question: Ask for Yes / No / Cancel

Same function, but if you hit escape key Esc, this will immediately quit the script (Note: exit could be replaced by return 2, to be checked by main script).

#/bin/sh
askFor() {
    __aF_yExpr="$(locale yesexpr)" __aF_nExpr="$(locale noexpr)"
    printf '%s? ' "$*"
    __aF_oStty=$(stty -g)
    stty raw -echo
    __aF_key=$( while ! head -c 1| grep -P "\e|$__aF_yExpr|$__aF_nExpr"; do :;done )
    stty $__aF_oStty
    if [ "$__aF_key" != "${__aF_key#${__aF_yExpr#^}}" ]; then
        echo Yes
        return 0
    elif [ "$__aF_key" != "${__aF_key#${__aF_nExpr#^}}" ]; then
        echo No
        return 1
    fi
    echo Cancel
    exit
}

You could use same sample script for testing this version.

2. Using dedicated tools

There are many tools which were built using libncurses, libgtk, libqt or other graphical libraries. For example, using whiptail:

if whiptail --yesno "Is this a good question" 20 60 ;then
    echo Yes
else
    echo No
fi

Depending on your system, you may need to replace whiptail with another similiar tool:

dialog --yesno "Is this a good question" 20 60 && echo Yes

gdialog --yesno "Is this a good question" 20 60 && echo Yes

kdialog --yesno "Is this a good question" 20 60 && echo Yes

where 20 is height of dialog box in number of lines and 60 is width of the dialog box. These tools all have near same syntax.

DIALOG=whiptail
if [ -x /usr/bin/gdialog ] ;then DIALOG=gdialog ; fi
if [ -x /usr/bin/xdialog ] ;then DIALOG=xdialog ; fi
...
$DIALOG --yesno ...

3. Bash specific solutions

Basic in line method

read -p "Is this a good question (y/n)? " answer
case ${answer:0:1} in
    y|Y )
        echo Yes
    ;;
    * )
        echo No
    ;;
esac

I prefer to use case so I could even test for yes | ja | si | oui if needed...

in line with single key feature

Under bash, we can specify the length of intended input for for the read command:

read -n 1 -p "Is this a good question (y/n)? " answer

Under bash, read command accepts a timeout parameter, which could be useful.

read -t 3 -n 1 -p "Is this a good question (Y/n)? " answer
[ -z "$answer" ] && answer="Yes"  # if 'yes' have to be default choice

Timeout with countdown:

i=6 ;while ((i-->1)) &&
! read -sn 1 -t 1 -p $'\rIs this a good question (Y/n)? '$i$'..\e[3D' answer;do
  :;done ;[[ $answer == [nN] ]] && answer=No || answer=Yes ;echo "$answer "

3. Some tricks for dedicated tools

More sophisticated dialog boxes, beyond simple yes - no purposes:

dialog --menu "Is this a good question" 20 60 12 y Yes n No m Maybe

Progress bar:

dialog --gauge "Filling the tank" 20 60 0 < <(
    for i in {1..100};do
        printf "XXX\n%d\n%(%a %b %T)T progress: %d\nXXX\n" $i -1 $i
        sleep .033
    done
) 

Little demo:

#!/bin/sh
while true ;do
    [ -x "$(which ${DIALOG%% *})" ] || DIALOG=dialog
    DIALOG=$($DIALOG --menu "Which tool for next run?" 20 60 12 2>&1 \
            whiptail       "dialog boxes from shell scripts" >/dev/tty \
            dialog         "dialog boxes from shell with ncurses" \
            gdialog        "dialog boxes from shell with Gtk" \
            kdialog        "dialog boxes from shell with Kde" ) || break
    clear;echo "Choosed: $DIALOG."
    for i in `seq 1 100`;do
        date +"`printf "XXX\n%d\n%%a %%b %%T progress: %d\nXXX\n" $i $i`"
        sleep .0125
      done | $DIALOG --gauge "Filling the tank" 20 60 0
    $DIALOG --infobox "This is a simple info box\n\nNo action required" 20 60
    sleep 3
    if $DIALOG --yesno  "Do you like this demo?" 20 60 ;then
        AnsYesNo=Yes; else AnsYesNo=No; fi
    AnsInput=$($DIALOG --inputbox "A text:" 20 60 "Text here..." 2>&1 >/dev/tty)
    AnsPass=$($DIALOG --passwordbox "A secret:" 20 60 "First..." 2>&1 >/dev/tty)
    $DIALOG --textbox /etc/motd 20 60
    AnsCkLst=$($DIALOG --checklist "Check some..." 20 60 12 \
        Correct "This demo is useful"        off \
        Fun        "This demo is nice"        off \
        Strong        "This demo is complex"        on 2>&1 >/dev/tty)
    AnsRadio=$($DIALOG --radiolist "I will:" 20 60 12 \
        " -1" "Downgrade this answer"        off \
        "  0" "Not do anything"                on \
        " +1" "Upgrade this anser"        off 2>&1 >/dev/tty)
    out="Your answers:\nLike: $AnsYesNo\nInput: $AnsInput\nSecret: $AnsPass"
    $DIALOG --msgbox "$out\nAttribs: $AnsCkLst\nNote: $AnsRadio" 20 60
  done

More samples? Have a look at Using whiptail for choosing USB device and USB removable storage selector: USBKeyChooser

5. Using readline's history

Example:

#!/bin/bash

set -i
HISTFILE=~/.myscript.history
history -c
history -r

myread() {
    read -e -p '> ' $1
    history -s ${!1}
}
trap 'history -a;exit' 0 1 2 3 6

while myread line;do
    case ${line%% *} in
        exit )  break ;;
        *    )  echo "Doing something with '$line'" ;;
      esac
  done

This will create a file .myscript.history in your $HOME directory, than you could use readline's history commands, like Up, Down, Ctrl+r and others.

6. Using new fzf utility

There are a new utility called fzf for fuzzy finder.

if [[ $(fzf --header='Delete current directory?' --tac <<<$'Yes\nNo'
        ) == Yes ]]; then
   echo rm .
fi

or

if [[ $(fzf --header='Delete current directory?' --tac < <(locale yesstr nostr
         )) == $(locale yesstr) ]]; then
    echo rm .
fi

This tool is very flexible and powerfull, using preview window and key binging.

Here is a sample, using catimg, pdftotext, batcat, ghostscript and w3m external tools, for populating preview window, then will populate a bash array for storing user selection:

#!/bin/bash
fPrev() { 
    printf "\e[40m%s\e[0m\n" "$(var="$1"; cd "${var%/*}"; ls -dhl "${var##*/}")";
    case $(file -b --mime-type "$1") in 
        text/html) w3m -T text/html -dump "$1" ;;
        text/x-*) batcat --color always "$1" ;;
        text*) cat "$1" ;;
        application/postscript) gs -sDEVICE=png16m -r60 -sOutputFile=- -q \
                -dNOPAUSE -dBATCH -dSAFER - -c quit < "$1" 2> /dev/null |
                       catimg -w $((2*COLUMNS)) - ;;
        image/svg+xml) inkscape -d 100 --export-type=png -o - "$1" |
                       catimg -w $((2*COLUMNS)) - ;;
        image*) catimg -w $((2*COLUMNS)) "$1" ;;
        application/pdf) pdftotext -layout - - < "$1" ;;
        inode/directory) /bin/ls --color=alway -bhlt "$1" |
                       sed "s/\( \+[^ ]\+\)\{3\}//" ;;
        *) cat -e "$1" ;;
    esac
}
export -f fPrev;
mapfile -td '' array < <(
    fzf -m -e -i --print0 --preview='fPrev {}' --preview-window='right,70%' < <(
        find "$@" -maxdepth 2 -exec ls -1dtr {} +
        ))
declare -p array

I posted here, another fzf / bash example, connecting to a database (sqlite3 for the sample), then use SQL requests for populating preview window. (Stored as a tarball compressed by zstandard, could be open by modern GNU tar, by tar -xf fzfDbDemo.tzst or by zstdcat fzfDbDemo.tzst | tar -x for old versions of tar.)

Upvotes: 723

Myrddin Emrys
Myrddin Emrys

Reputation: 44005

A widely available method to get user input at a shell prompt is the read command. Here is a demonstration:

while true; do
    read -p "Do you wish to install this program? " yn
    case $yn in
        [Yy]* ) make install; break;;
        [Nn]* ) exit;;
        * ) echo "Please answer yes or no.";;
    esac
done

Another method, pointed out by Steven Huwig, is Bash's select command. Here is the same example using select:

echo "Do you wish to install this program?"
select yn in "Yes" "No"; do
    case $yn in
        Yes ) make install; break;;
        No ) exit;;
    esac
done

With select you don't need to sanitize the input – it displays the available choices, and you type a number corresponding to your choice. It also loops automatically, so there's no need for a while true loop to retry if they give invalid input.

Also, Léa Gris demonstrated a way to make the request language agnostic in her answer. Adapting my first example to better serve multiple languages might look like this:

set -- $(locale LC_MESSAGES)
yesexpr="$1"; noexpr="$2"; yesword="$3"; noword="$4"

while true; do
    read -p "Install (${yesword} / ${noword})? " yn
    if [[ "$yn" =~ $yesexpr ]]; then make install; exit; fi
    if [[ "$yn" =~ $noexpr ]]; then exit; fi
    echo "Answer ${yesword} / ${noword}."
done

Obviously other communication strings remain untranslated here (Install, Answer) which would need to be addressed in a more fully completed translation, but even a partial translation would be helpful in many cases.

Finally, please check out the excellent answer by F. Hauri.

Upvotes: 2079

ggorlen
ggorlen

Reputation: 57195

Here's yet another approach with a function that returns 0 for success, 1 for failure. There's no re-prompt; y/Y confirms and anything else aborts.

#!/usr/bin/env bash

set -eu -o pipefail

function prompt() {
    read -p "$* [Y/n]: " yn
    if [[ $yn = "y" || $yn = "Y" ]]; then
        return 0
    else 
        return 1
    fi
}

prompt "one liner?" && echo "YES" || echo "NO"

if prompt "in an if?"; then
    echo "YES"
else
    echo "NO"
fi

Upvotes: 1

James Resoldier
James Resoldier

Reputation: 33

I created this function to be called by all my bash scripts that require a yes/no/true/false/default response, as I was tired of recreating the code every time I needed a response.

It has a settable question, and settable default answer with error handling for the default response (meaning you can change what the default answer is), as well as capturing questions that fail to be surrounded by quotes. I realize this is more detailed than the original post asked for, but this should be fully portable in any bash 4+. It is usable as a command in if statements as well, as it returns 0 or 1, as well as detailing back what the answer was for debugging.

Usage: confirm -d n "Is this the answer?"

Which returns the output:

user@ubuntu# confirm -d n "Is this the answer?"
Is this the answer? [Y/n:default=N]: 
default no

Here's the function:

confirm(){
  errorMsg='
  Too many arguments.
  use "-d" as the first option to set default value.
  If providing a question, remember to quote the line to prevent each word from being an argument.
  example: confirm -d n "Is this the answer?"'
  defSet=false
  while [ "$#" -ge 1 ]; do
    if [ "$#" -ge 2 ]; then
      confirmArg=$1
      if [[ "$confirmArg" = '-d' ]]; then
        confirmArgCheck=$2
        errorMsg2="
        '${confirmArgCheck}' is not a valid default return value.
        Must be [Yy]es/[Nn]o or [Tt]rue/[Ff]alse"
        case $confirmArgCheck in
          [Yy]*|[Tt]*)
            regex1="[Yy][Ee]?[Ss]?$"
            regex2="[Tt][Rr]?[Uu]?[Ee]?$"
            if [[ "$confirmArgCheck" =~ $regex1 ]]; then
              unset regex1 && unset regex2
              confirmArgVal=Y
            elif [[ "$confirmArgCheck" =~ $regex2 ]]; then
              unset regex1 && unset regex2
              confirmArgVal=Y
            else
              unset regex1 && unset regex2
              echo "$errorMsg2"
              return 1
            fi
            ;;
          [Nn]*|[Ff]*)
            regex1="[Nn][Oo]?$"
            regex2="[Ff][Aa]?[Ll]?[Ss]?[Ee]?$"
            if [[ "$confirmArgCheck" =~ $regex1 ]]; then
              unset regex1 && unset regex2
              confirmArgVal=N
            elif [[ "$confirmArgCheck" =~ $regex2 ]]; then
              unset regex1 && unset regex2
              confirmArgVal=N
            else
              unset regex1 && unset regex2
              echo "$errorMsg2"
              return 1
            fi
            ;;
          *)
            echo "$errorMsg2"
            return 1
            ;;
        esac
        defSet=true
        shift 2
      else
        if [[ ! "$*" == *"-d"* ]]; then
          shiftmore="$#"
          quest="$@"
          questSet=true
          shift "$shiftmore"
        else
          echo "$errorMsg"
          return 1
        fi
      fi
    else
      quest=$1
      questSet=true
      shift 1
    fi
  done
  if [[ ! $defSet = true ]]; then
    confirmArgVal=Y
  fi
  if [[ ! $questSet = true ]]; then
    quest='Answer?'
  fi
  while true; do
    read -r -p "${quest} [Y/n:default=$confirmArgVal]: " yn
    case $yn in
      [Yy]*)
        unset quest
        unset yn
        unset confirmArgVal
        echo "answered yes"
        return 0
        ;;
      [Nn]*)
        unset quest
        unset yn
        unset confirmArgVal
        echo "answered no"
        return 1
        ;;
      "")
        unset quest
        unset yn
        if [[ "$confirmArgVal" = 'Y' ]]; then
          unset confirmArgVal
          echo "default yes"
          return 0
        else
          unset confirmArgVal
          echo "default no"
          return 1
        fi
        ;;
      *)
        echo "Please answer yes or no."
        ;;
    esac
  done
}

Upvotes: 0

Roland
Roland

Reputation: 5232

The absolute most simple solution is this one-liner without clever tricks:

read -p "press enter ..." y

It reminds of the classic DOS Hit any key to continue, except that it waits for the Enter key, not just any key.

True, this does not offer you three options for Yes No Cancel, but it is useful where you accept control-C as No resp. Cancel in simple scripts like, e.g.:

#!/bin/sh
echo Backup this project
read -p "press enter ..." y
rsync -tavz . /media/hard_to_remember_path/backup/projects/yourproject/

because you don't like to need to remember ugly commands and paths, but neither scripts that run too fast, without giving you a chance to stop before you decide it is not the script you intended to run.

The command line argument y is required with sh and can optionally be used to receive the answer typed by the user before pressing the Enter key, like this:

echo You entered $y

With bash you may omit the last argument and just use:

read -p "press enter ..."

Upvotes: 4

Steven Huwig
Steven Huwig

Reputation: 20794

Bash has select for this purpose. Here's how you would use it in a script:

select result in Yes No Cancel
do
    echo $result
done

This is what it would look like to use:

$ bash examplescript.sh
1) Yes
2) No
3) Cancel
#? 1
Yes
#? 2
No
#? 3
Cancel
#?

Upvotes: 129

Erik Pukinskis
Erik Pukinskis

Reputation: 520

Lots of good answers to this question, but from what I can see none of them are my ideal, which would:

  1. Be simple, just a couple lines of shell
  2. Work with a single y/n keypress (no need to press enter)
  3. Default to yes if you just hit enter
  4. Work with an uppercase Y/N as well

Here's my version which does has those properties:

read -n1 -p "Continue? (Y/n) " confirm

if ! echo $confirm | grep '^[Yy]\?$'; then
  exit 1
fi

You can modify that conditional to only run on "yes" (just remove the ! in the if statement) or add an else if you want to run code on both branches.

Upvotes: 12

chovy
chovy

Reputation: 75804

You can write a function to test:

confirm() {
  local ans IFS=;
  while read -rp "$1" -n1 ans;
  do printf '\n';
    case $ans in [Yy]) return 0;;
      [Nn]) return 1;;
    esac;
  done;
}; ## Usage: if confirm "Are you sure? "; then ...

if confirm "Does everything look ok...reboot now? [Y/n]"; then
  echo "rebooting..."
  sleep 5
  reboot
fi

Upvotes: 1

Diego Torres Milano
Diego Torres Milano

Reputation: 69388

A one-liner python alternative using PyInquirer

python3 -c 'import PyInquirer; print(PyInquirer.prompt([{"type":"confirm", "message":"Do you want to continue?", "name":"r"}]).get("r"))'

which supports yes/no/cancel (intr, CTRL+C).

enter image description here

Upvotes: 1

user9869932
user9869932

Reputation: 7377

In my case I needed to read from a downloaded script i.e.,

curl -Ss https://example.com/installer.sh | sh

The line read -r yn </dev/tty allowed it to read input in this case.

printf "These files will be uploaded. Is this ok? (y/N) "
read -r yn </dev/tty

if [ "$yn" = "y" ]; then
   
   # Yes
else
   
   # No
fi

Upvotes: 16

Ctrl-C
Ctrl-C

Reputation: 4172

One-liner:

read -p "Continue? [Enter] → Yes, [Ctrl]+[C] → No."

This assumes that "No" and "Cancel" have the same outcome, so no reason to treat them differently.

Upvotes: 10

ish-west
ish-west

Reputation: 111

I've made this small script for yes/no questions: https://github.com/optimistiCli/getans

Example:

#!/bin/bash

if ! getans.sh 'Shall we proceed?' y ; then
    echo "User said “NO”"
    exit 1
fi

echo "User said “YES”"
# do something usefull
exit 0

Direct link: https://github.com/optimistiCli/getans/raw/main/getans.sh

Upvotes: 1

Samir Kape
Samir Kape

Reputation: 2075

Check this

read -p "Continue? (y/n): " confirm && [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]] || exit 1

Upvotes: 6

rubo77
rubo77

Reputation: 20865

This is what I usually need in a script/function:

  • default answer is Yes, if you hit ENTER
  • accept also z (in case you mix up you are on QWERTZ Layout)
  • accept other lanyuages ("ja", "Oui", ...)
  • handle the right exit in case you are inside a function
while true; do
    read -p "Continue [Y/n]? " -n 1 -r -e yn
    case "${yn:-Y}" in
        [YyZzOoJj]* ) echo; break ;;
        [Nn]* ) [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 ;; # handle exits from shell or function but don't exit interactive shell
        * ) echo "Please answer yes or no.";;
    esac
done
echo "and off we go!"

Upvotes: 3

L&#233;a Gris
L&#233;a Gris

Reputation: 19665

It is possible to handle a locale-aware "Yes / No choice" in a POSIX shell; by using the entries of the LC_MESSAGES locale category, witch provides ready-made RegEx patterns to match an input, and strings for localized Yes No.

#!/usr/bin/env sh

# Getting LC_MESSAGES values into variables
# shellcheck disable=SC2046 # Intended IFS splitting
IFS='
' set -- $(locale LC_MESSAGES)

yesexpr="$1"
noexpr="$2"
yesstr="$3"
nostr="$4"
messages_codeset="$5" # unused here, but kept as documentation

# Display Yes / No ? prompt into locale
echo "$yesstr / $nostr ?"

# Read answer
read -r yn

# Test answer
case "$yn" in
# match only work with the character class from the expression
  ${yesexpr##^}) echo "answer $yesstr" ;;
  ${noexpr##^}) echo "answer $nostr" ;;
esac

EDIT: As @Urhixidur mentioned in his comment:

Unfortunately, POSIX only specifies the first two (yesexpr and noexpr). On Ubuntu 16, yesstr and nostr are empty.

See: https://www.ee.ryerson.ca/~courses/ele709/susv4/xrat/V4_xbd_chap07.html#tag_21_07_03_06

LC_MESSAGES

The yesstr and nostr locale keywords and the YESSTR and NOSTR langinfo items were formerly used to match user affirmative and negative responses. In POSIX.1-2008, the yesexpr, noexpr, YESEXPR, and NOEXPR extended regular expressions have replaced them. Applications should use the general locale-based messaging facilities to issue prompting messages which include sample desired responses.

Alternatively using locales the Bash way:

#!/usr/bin/env bash

IFS=$'\n' read -r -d '' yesexpr noexpr _ < <(locale LC_MESSAGES)

printf -v yes_or_no_regex "(%s)|(%s)" "$yesexpr" "$noexpr"

printf -v prompt $"Please answer Yes (%s) or No (%s): " "$yesexpr" "$noexpr"

declare -- answer=;

until [[ "$answer" =~ $yes_or_no_regex ]]; do
  read -rp "$prompt" answer
done

if [[ -n "${BASH_REMATCH[1]}" ]]; then
  echo $"You answered: Yes"
else
  echo $"No, was your answer."
fi

The answer is matched using locale environment's provided regexps.

To translate the remaining messages, use bash --dump-po-strings scriptname to output the po strings for localization:

#: scriptname:8
msgid "Please answer Yes (%s) or No (%s): "
msgstr ""
#: scriptname:17
msgid "You answered: Yes"
msgstr ""
#: scriptname:19
msgid "No, was your answer."
msgstr ""

Upvotes: 22

Thorsten Staerk
Thorsten Staerk

Reputation: 1156

To get a nice ncurses-like inputbox use the command dialog like this:

#!/bin/bash
if (dialog --title "Message" --yesno "Want to do something risky?" 6 25)
# message box will have the size 25x6 characters
then 
    echo "Let's do something risky"
    # do something risky
else 
    echo "Let's stay boring"
fi

The dialog package is installed by default at least with SUSE Linux. Looks like: the "dialog" command in action

Upvotes: 21

yPhil
yPhil

Reputation: 8387

You can use the built-in read command ; Use the -p option to prompt the user with a question.

Since BASH4, you can now use -i to suggest an answer :

read -e -p "Enter the path to the file: " -i "/usr/local/etc/" FILEPATH
echo $FILEPATH

(But remember to use the "readline" option -e to allow line editing with arrow keys)

If you want a "yes / no" logic, you can do something like this:

read -e -p "
List the content of your home dir ? [Y/n] " YN

[[ $YN == "y" || $YN == "Y" || $YN == "" ]] && ls -la ~/

Upvotes: 179

Walter A
Walter A

Reputation: 20032

You can use the default REPLY on a read, convert to lowercase and compare to a set of variables with an expression.
The script also supports ja/si/oui

read -rp "Do you want a demo? [y/n/c] "

[[ ${REPLY,,} =~ ^(c|cancel)$ ]] && { echo "Selected Cancel"; exit 1; }

if [[ ${REPLY,,} =~ ^(y|yes|j|ja|s|si|o|oui)$ ]]; then
   echo "Positive"
fi

Upvotes: 13

Tom Hale
Tom Hale

Reputation: 46983

Single keypress only

Here's a longer, but reusable and modular approach:

  • Returns 0=yes and 1=no
  • No pressing enter required - just a single character
  • Can press enter to accept the default choice
  • Can disable default choice to force a selection
  • Works for both zsh and bash.

Defaulting to "no" when pressing enter

Note that the N is capitalsed. Here enter is pressed, accepting the default:

$ confirm "Show dangerous command" && echo "rm *"
Show dangerous command [y/N]?

Also note, that [y/N]? was automatically appended. The default "no" is accepted, so nothing is echoed.

Re-prompt until a valid response is given:

$ confirm "Show dangerous command" && echo "rm *"
Show dangerous command [y/N]? X
Show dangerous command [y/N]? y
rm *

Defaulting to "yes" when pressing enter

Note that the Y is capitalised:

$ confirm_yes "Show dangerous command" && echo "rm *"
Show dangerous command [Y/n]?
rm *

Above, I just pressed enter, so the command ran.

No default on enter - require y or n

$ get_yes_keypress "Here you cannot press enter. Do you like this [y/n]? "
Here you cannot press enter. Do you like this [y/n]? k
Here you cannot press enter. Do you like this [y/n]?
Here you cannot press enter. Do you like this [y/n]? n
$ echo $?
1

Here, 1 or false was returned. Note that with this lower-level function you'll need to provide your own [y/n]? prompt.

Code

# Read a single char from /dev/tty, prompting with "$*"
# Note: pressing enter will return a null string. Perhaps a version terminated with X and then remove it in caller?
# See https://unix.stackexchange.com/a/367880/143394 for dealing with multi-byte, etc.
function get_keypress {
  local REPLY IFS=
  >/dev/tty printf '%s' "$*"
  [[ $ZSH_VERSION ]] && read -rk1  # Use -u0 to read from STDIN
  # See https://unix.stackexchange.com/q/383197/143394 regarding '\n' -> ''
  [[ $BASH_VERSION ]] && </dev/tty read -rn1
  printf '%s' "$REPLY"
}

# Get a y/n from the user, return yes=0, no=1 enter=$2
# Prompt using $1.
# If set, return $2 on pressing enter, useful for cancel or defualting
function get_yes_keypress {
  local prompt="${1:-Are you sure [y/n]? }"
  local enter_return=$2
  local REPLY
  # [[ ! $prompt ]] && prompt="[y/n]? "
  while REPLY=$(get_keypress "$prompt"); do
    [[ $REPLY ]] && printf '\n' # $REPLY blank if user presses enter
    case "$REPLY" in
      Y|y)  return 0;;
      N|n)  return 1;;
      '')   [[ $enter_return ]] && return "$enter_return"
    esac
  done
}

# Credit: http://unix.stackexchange.com/a/14444/143394
# Prompt to confirm, defaulting to NO on <enter>
# Usage: confirm "Dangerous. Are you sure?" && rm *
function confirm {
  local prompt="${*:-Are you sure} [y/N]? "
  get_yes_keypress "$prompt" 1
}    

# Prompt to confirm, defaulting to YES on <enter>
function confirm_yes {
  local prompt="${*:-Are you sure} [Y/n]? "
  get_yes_keypress "$prompt" 0
}

Upvotes: 14

Mike Q
Mike Q

Reputation: 7337

In response to others:

You don't need to specify case in BASH4 just use the ',,' to make a var lowercase. Also I strongly dislike putting code inside of the read block, get the result and deal with it outside of the read block IMO. Also include a 'q' for quit IMO. Lastly why type 'yes' just use -n1 and have the press y.

Example: user can press y/n and also q to just quit.

ans=''
while true; do
    read -p "So is MikeQ the greatest or what (y/n/q) ?" -n1 ans
    case ${ans,,} in
        y|n|q) break;;
        *) echo "Answer y for yes / n for no  or q for quit.";;
    esac
done

echo -e "\nAnswer = $ans"

if [[ "${ans,,}" == "q" ]] ; then
        echo "OK Quitting, we will assume that he is"
        exit 0
fi

if [[ "${ans,,}" == "y" ]] ; then
        echo "MikeQ is the greatest!!"
else
        echo "No? MikeQ is not the greatest?"
fi

Upvotes: 3

Eduardo Cuomo
Eduardo Cuomo

Reputation: 19006

Yes / No / Cancel

Function

#!/usr/bin/env bash
@confirm() {
  local message="$*"
  local result=''

  echo -n "> $message (Yes/No/Cancel) " >&2

  while [ -z "$result" ] ; do
    read -s -n 1 choice
    case "$choice" in
      y|Y ) result='Y' ;;
      n|N ) result='N' ;;
      c|C ) result='C' ;;
    esac
  done

  echo $result
}

Usage

case $(@confirm 'Confirm?') in
  Y ) echo "Yes" ;;
  N ) echo "No" ;;
  C ) echo "Cancel" ;;
esac

Confirm with clean user input

Function

#!/usr/bin/env bash
@confirm() {
  local message="$*"
  local result=3

  echo -n "> $message (y/n) " >&2

  while [[ $result -gt 1 ]] ; do
    read -s -n 1 choice
    case "$choice" in
      y|Y ) result=0 ;;
      n|N ) result=1 ;;
    esac
  done

  return $result
}

Usage

if @confirm 'Confirm?' ; then
  echo "Yes"
else
  echo "No"
fi

Upvotes: 3

oHo
oHo

Reputation: 54661

You want:

  • Bash builtin commands (i.e. portable)
  • Check TTY
  • Default answer
  • Timeout
  • Colored question

Snippet

do_xxxx=y                      # In batch mode => Default is Yes
[[ -t 0 ]] &&                  # If TTY => Prompt the question
read -n 1 -p $'\e[1;32m
Do xxxx? (Y/n)\e[0m ' do_xxxx  # Store the answer in $do_xxxx
if [[ $do_xxxx =~ ^(y|Y|)$ ]]  # Do if 'y' or 'Y' or empty
then
    xxxx
fi

Explanations

  • [[ -t 0 ]] && read ... => Call command read if TTY
  • read -n 1 => Wait for one character
  • $'\e[1;32m ... \e[0m ' => Print in green
    (green is fine because readable on both white/black backgrounds)
  • [[ $do_xxxx =~ ^(y|Y|)$ ]] => bash regex

Timeout => Default answer is No

do_xxxx=y
[[ -t 0 ]] && {                   # Timeout 5 seconds (read -t 5)
read -t 5 -n 1 -p $'\e[1;32m
Do xxxx? (Y/n)\e[0m ' do_xxxx ||  # read 'fails' on timeout
do_xxxx=n ; }                     # Timeout => answer No
if [[ $do_xxxx =~ ^(y|Y|)$ ]]
then
    xxxx
fi

Upvotes: 34

ThatLinuxGuy
ThatLinuxGuy

Reputation: 239

Use the read command:

echo Would you like to install? "(Y or N)"

read x

# now check if $x is "y"
if [ "$x" = "y" ]; then
    # do something here!
fi

and then all of the other stuff you need

Upvotes: 23

Apurv Nerlekar
Apurv Nerlekar

Reputation: 2320

The easiest way to achieve this with the least number of lines is as follows:

read -p "<Your Friendly Message here> : y/n/cancel" CONDITION;

if [ "$CONDITION" == "y" ]; then
   # do something here!
fi

The if is just an example: it is up to you how to handle this variable.

Upvotes: 28

Yokai
Yokai

Reputation: 1220

I noticed that no one posted an answer showing multi-line echo menu for such simple user input so here is my go at it:

#!/bin/bash

function ask_user() {    

echo -e "
#~~~~~~~~~~~~#
| 1.) Yes    |
| 2.) No     |
| 3.) Quit   |
#~~~~~~~~~~~~#\n"

read -e -p "Select 1: " choice

if [ "$choice" == "1" ]; then

    do_something

elif [ "$choice" == "2" ]; then

    do_something_else

elif [ "$choice" == "3" ]; then

    clear && exit 0

else

    echo "Please select 1, 2, or 3." && sleep 3
    clear && ask_user

fi
}

ask_user

This method was posted in the hopes that someone may find it useful and time-saving.

Upvotes: 8

SumoRunner
SumoRunner

Reputation: 884

inquire ()  {
  echo  -n "$1 [y/n]? "
  read answer
  finish="-1"
  while [ "$finish" = '-1' ]
  do
    finish="1"
    if [ "$answer" = '' ];
    then
      answer=""
    else
      case $answer in
        y | Y | yes | YES ) answer="y";;
        n | N | no | NO ) answer="n";;
        *) finish="-1";
           echo -n 'Invalid response -- please reenter:';
           read answer;;
       esac
    fi
  done
}

... other stuff

inquire "Install now?"

...

Upvotes: 47

oOpSgEo
oOpSgEo

Reputation: 193

I've used the case statement a couple of times in such a scenario, using the case statment is a good way to go about it. A while loop, that ecapsulates the case block, that utilizes a boolean condition can be implemented in order to hold even more control of the program, and fulfill many other requirements. After the all the conditions have been met, a break can be used which will pass control back to the main part of the program. Also, to meet other conditions, of course conditional statements can be added to accompany the control structures: case statement and possible while loop.

Example of using a case statement to fulfill your request

#! /bin/sh 

# For potential users of BSD, or other systems who do not
# have a bash binary located in /bin the script will be directed to
# a bourne-shell, e.g. /bin/sh

# NOTE: It would seem best for handling user entry errors or
# exceptions, to put the decision required by the input 
# of the prompt in a case statement (case control structure), 

echo Would you like us to perform the option: "(Y|N)"

read inPut

case $inPut in
    # echoing a command encapsulated by 
    # backticks (``) executes the command
    "Y") echo `Do something crazy`
    ;;
    # depending on the scenario, execute the other option
    # or leave as default
    "N") echo `execute another option`
    ;;
esac

exit

Upvotes: 3

more generic would be:

function menu(){
    title="Question time"
    prompt="Select:"
    options=("Yes" "No" "Maybe")
    echo "$title"
    PS3="$prompt"
    select opt in "${options[@]}" "Quit/Cancel"; do
        case "$REPLY" in
            1 ) echo "You picked $opt which is option $REPLY";;
            2 ) echo "You picked $opt which is option $REPLY";;
            3 ) echo "You picked $opt which is option $REPLY";;
            $(( ${#options[@]}+1 )) ) clear; echo "Goodbye!"; exit;;
            *) echo "Invalid option. Try another one.";continue;;
         esac
     done
     return
}

Upvotes: 4

ccDict
ccDict

Reputation: 653

As a friend of a one line command I used the following:

while [ -z $prompt ]; do read -p "Continue (y/n)?" choice;case "$choice" in y|Y ) prompt=true; break;; n|N ) exit 0;; esac; done; prompt=;

Written longform, it works like this:

while [ -z $prompt ];
  do read -p "Continue (y/n)?" choice;
  case "$choice" in
    y|Y ) prompt=true; break;;
    n|N ) exit 0;;
  esac;
done;
prompt=;

Upvotes: 3

Related Questions