Bash Yes No Prompt: 7 Proven Patterns and 5 Mistakes

A bash yes no prompt is a read -r -p "Question? [y/N] " ans followed by a case on the answer. The short version that most tutorials show works for a quick test, but it treats "yolo" as yes, loops forever when there is no terminal and silently skips the question when the script is piped. This guide shows a confirm function that handles all of that, plus Yes/No/Cancel, single-key answers, timeouts, select menus and unattended runs with -y.

Every example was run on 2026-10-23 in a debian:trixie container with GNU bash 5.2.37 and dash as /bin/sh. The answers were typed on a real pseudo-terminal (util-linux script), so the output below is exactly what a user sees at each bash yes no prompt, including the typed answer.

The classic bash yes no prompt and its problems

This is the version found in many old posts, including the previous version of this article:

#!/bin/bash
read -p "Do you want to proceed? (y) Yes / (n) No / (c) Cancel: " choice
case $choice in
  [yY]*) echo "You chose Yes. Proceeding..." ;;
  [nN]*) echo "You chose No. Action canceled." ;;
  [cC]*) echo "You chose Cancel. Exiting."; exit 0 ;;
  *)     echo "Invalid input."; exit 1 ;;
esac

Normal answers work:

$ ./basic.sh
Do you want to proceed? (y) Yes / (n) No / (c) Cancel: y
You chose Yes. Proceeding...

$ ./basic.sh
Do you want to proceed? (y) Yes / (n) No / (c) Cancel: maybe
Invalid input.
[exit 1]

The pattern [yY]* means "starts with y", so any word that starts with y or n is accepted as an answer:

$ ./basic.sh
Do you want to proceed? (y) Yes / (n) No / (c) Cancel: yolo
You chose Yes. Proceeding...

$ ./basic.sh
Do you want to proceed? (y) Yes / (n) No / (c) Cancel: nope, wait
You chose No. Action canceled.

For a prompt that guards rm -rf or a reboot, match whole words instead: y|yes and n|no. Pressing Enter gives an empty string, which falls into *) here; a good bash yes no prompt turns Enter into a default answer instead.

Two more surprises show up when the input does not come from a keyboard. With a pipe, the prompt text is not printed at all, because read -p only shows the prompt when input comes from a terminal. With no input (/dev/null, a cron job, a CI runner), read gets end of file and the variable is empty:

$ echo y | ./basic.sh
You chose Yes. Proceeding...
[exit 0]

$ ./basic.sh < /dev/null
Invalid input.
[exit 1]

A reusable bash yes no prompt function with a default

This bash yes no prompt function asks until it gets a clear answer, uses a default on Enter, ignores case with ${ans,,} (bash 4+), and treats end of file or a 30 second timeout as "no":

#!/bin/bash
# confirm: ask until the answer is yes/no; Enter = default; EOF or timeout = no
confirm() {
  local prompt=$1 default=${2:-n} ans
  local hint="[y/N]"; [[ $default == y ]] && hint="[Y/n]"
  while true; do
    if ! read -r -t 30 -p "$prompt $hint " ans; then
      echo; echo "no answer, using 'no'" >&2; return 1
    fi
    ans=${ans:-$default}
    case ${ans,,} in
      y|yes) return 0 ;;
      n|no)  return 1 ;;
      *)     echo "Please answer y or n." ;;
    esac
  done
}
if confirm "Delete /tmp/demo?" n; then echo "deleting"; else echo "kept"; fi

The capital letter in [y/N] tells the user which answer Enter picks. Results from the lab:

$ ./strict.sh
Delete /tmp/demo? [y/N] YES
deleting

$ ./strict.sh
Delete /tmp/demo? [y/N]
kept

$ ./strict.sh
Delete /tmp/demo? [y/N] x
Please answer y or n.
Delete /tmp/demo? [y/N] n
kept

$ ./strict.sh < /dev/null

no answer, using 'no'
kept

Because confirm returns 0 or 1, you can use it directly in if, && or ||, the same way as any command exit code (see check exit codes in bash). Use -r on every read so a backslash in the answer is kept as typed.

bash yes no prompt with a default answer and input validation in a terminal
A bash yes no prompt from the lab: invalid answer, retry, default on Enter.

Bash yes no prompt with Cancel in a loop

When "no" should only skip one step and "cancel" should stop the whole script, use three answers and a different exit code for cancel. The || { ...; } after read handles Ctrl+D:

#!/bin/bash
while true; do
  read -r -p "Apply changes? [y]es / [n]o / [c]ancel: " ans || { echo; echo "EOF: cancel"; exit 130; }
  case ${ans,,} in
    y|yes)    echo "applying"; break ;;
    n|no)     echo "skipping this step"; break ;;
    c|cancel) echo "cancelled"; exit 130 ;;
    *)        echo "Invalid answer: '$ans'" ;;
  esac
done
echo "script continues (rc so far 0)"
$ ./ync.sh
Apply changes? [y]es / [n]o / [c]ancel: later
Invalid answer: 'later'
Apply changes? [y]es / [n]o / [c]ancel: c
cancelled
[exit 130]

$ ./ync.sh          # Ctrl+D pressed
Apply changes? [y]es / [n]o / [c]ancel:
EOF: cancel
[exit 130]

Without that || branch a bash yes no prompt loop never ends when stdin is empty. This loop has no EOF check and was stopped by a counter in the lab after 100,000 empty reads in well under 20 seconds:

while true; do
  read -p "Continue? (y/n) " a
  case $a in [yY]*) break ;; [nN]*) exit 1 ;; esac
done

$ timeout 20 ./loop_bug.sh < /dev/null
still looping after 100000 reads
[exit 2]

More on combining while and case: bash script while loop with case statement.

Single key, timeout and select menu

For a one-key bash yes no prompt, read -n 1 returns after one character, so the user does not press Enter. Add an echo afterwards, because the cursor stays on the prompt line:

read -r -n 1 -p "Reboot now? [y/N] " k; echo
[[ $k == [yY] ]] && echo "rebooting (demo)" || echo "not rebooting"

$ ./onekey.sh
Reboot now? [y/N] y
rebooting (demo)

In a bash yes no prompt, read -t gives up after the given number of seconds. In bash 5.2 the exit status on timeout was 142 (the manual only promises "greater than 128"). With set -e that status ends the script right at the read:

$ ./timeout.sh       # nothing typed for 2 s
Answer within 2 s: rc=142

$ cat sete.sh
#!/bin/bash
set -e
read -r -t 2 -p "Continue? [y/N] " ans
echo "this line runs only if read succeeded"

$ ./sete.sh          # nothing typed
Continue? [y/N] [exit 142]

For a numbered menu, bash has select. It prints the list, reads a number into $REPLY and repeats on an invalid number until you break:

PS3="Choose 1-3: "
select opt in Yes No Cancel; do
  case $opt in
    Yes) echo "yes"; break ;;
    No) echo "no"; break ;;
    Cancel) echo "cancel"; exit 130 ;;
    *) echo "invalid choice: $REPLY" ;;
  esac
done

$ ./select.sh
1) Yes
2) No
3) Cancel
Choose 1-3: 5
invalid choice: 5
Choose 1-3: 2
no

Bash yes no prompt in pipes, cron and CI

If the script reads data from stdin, the bash yes no prompt reads from the same pipe. In this test the script first reads a list of names, then asks; read gets end of file and returns 1 with an empty answer. Reading the answer from /dev/tty asks the keyboard instead, but that fails when there is no terminal at all:

mapfile -t files
echo "got ${#files[@]} names"
read -r -p "Delete them? [y/N] " ans
echo "answer='$ans' rc=$?"
read -r -p "Delete them (from /dev/tty)? [y/N] " ans </dev/tty

$ printf 'a\nb\n' | ./pipe.sh          # run from a terminal
got 2 names
answer='' rc=1
Delete them (from /dev/tty)? [y/N] y
answer='y'

$ printf 'a\nb\n' | ./pipe.sh          # no terminal (container without -t)
got 2 names
answer='' rc=1
./pipe.sh: line 7: /dev/tty: No such device or address

For scripts that also run from cron or CI, check for a terminal with [[ -t 0 ]] and offer a -y flag, like apt-get -y:

assume_yes=0; [[ ${1:-} == -y ]] && assume_yes=1
if (( assume_yes )); then ans=y
elif [[ -t 0 ]]; then read -r -p "Proceed? [y/N] " ans
else echo "stdin is not a terminal: use -y to run unattended" >&2; exit 2; fi
[[ ${ans,,} == y* ]] && echo "proceeding" || echo "aborted"

$ ./flag.sh < /dev/null
stdin is not a terminal: use -y to run unattended
[exit 2]

$ ./flag.sh -y < /dev/null
proceeding

POSIX sh version of a bash yes no prompt

${ans,,}, [[ ]], select and read -t are bash features. A script with #!/bin/sh runs under dash on Debian and Ubuntu, so use printf for the prompt and list the letter cases yourself. dash does accept read -p (the lab printed the prompt on a terminal), but other sh implementations may not, so printf is the portable way:

#!/bin/sh
printf 'Continue? [y/N] '
read ans
case $ans in [Yy]|[Yy][Ee][Ss]) echo yes ;; *) echo no ;; esac

$ sh ./posix.sh
Continue? [y/N] y
yes

Read options used in a bash yes no prompt

OptionWhat it does (from help read, bash 5.2)
-p promptoutput the prompt without a trailing newline (only when input is a terminal)
-rdo not allow backslashes to escape any characters
-n ncharsreturn after reading NCHARS characters rather than waiting for a newline
-sdo not echo input coming from a terminal (passwords)
-t timeouttime out and return failure; the exit status is greater than 128 on timeout

The full list is in the GNU Bash manual, Bash Builtins, and case patterns are described under Conditional Constructs.

To sum up: a solid bash yes no prompt uses read -r -p, matches whole words, picks a default on Enter, handles end of file and timeouts, and never waits for a keyboard that is not there. Copy the confirm function above and reuse it in every script that changes something.

Comments

Popular posts from this blog

How to Play Minecraft Bedrock Edition on Linux: A Comprehensive Guide for Tech Professionals

The Ultimate Guide: How to Set Up DXVK in Wine on Linux for Enhanced Gaming Performance

Linux desktop frozen? My 5 go-to tricks to try - before forcing a hard reboot