yesno() in Bash: Ask a Yes/No Question Safely
Install scripts and maintenance jobs often need one confirmation before doing something destructive: "Delete old backups? [y/N]". Bash has no built-in for that, but read -p plus a case statement makes a small function you can add to any script. It must handle careless typing and an empty Enter. It must also handle input that does not come from a person at all, which is the part most versions forget.
The function
# yesno() -- ask a yes/no question; 0 = yes, 1 = no, 2 = no answer/usage
# Usage: yesno "prompt" [y|n] (the optional default is used on Enter/EOF)
yesno() {
local prompt="$1" default="${2:-}" hint answer
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
printf 'Usage: yesno "prompt" [y|n]\n' >&2
return 2
fi
case "$default" in
[Yy]) hint="[Y/n]" ;;
[Nn]) hint="[y/N]" ;;
"") hint="[y/n]" ;;
*) printf 'yesno: default must be y or n\n' >&2; return 2 ;;
esac
while :; do
if ! read -r -p "$prompt $hint " answer && [ -z "$answer" ]; then
# EOF: nothing more will ever arrive, so never loop here
[ -n "$default" ] || return 2
answer="$default"
fi
[ -z "$answer" ] && answer="$default"
case "$answer" in
[Yy]|[Yy][Ee][Ss]) return 0 ;;
[Nn]|[Nn][Oo]) return 1 ;;
esac
printf "Please answer 'yes' or 'no'.\n" >&2
done
}How it works
The optional second argument is the default answer. It decides the hint shown after the prompt. The usual convention is that the capital letter is what Enter gives you: [Y/n], [y/N], or [y/n] when there is no default and the user must type something.
read -r -p prints the prompt and reads one line. -r stops backslashes being treated as escapes. Because IFS is left at its default, read also strips leading and trailing spaces and tabs, so " y " arrives as y.
The case patterns do the case-insensitive matching. [Yy][Ee][Ss] matches yes, YES and Yes without lowercasing anything first. I could have written ${answer,,}, but that needs Bash 4 and the stock /bin/bash on macOS is still 3.2. Bracket patterns work everywhere, including plain sh.
The important line is the if ! read ... test. read returns non-zero when it hits end of file. If it also read nothing, no answer is ever coming: the function uses the default if there is one, and otherwise returns 2. It never goes round the loop again on EOF. The && [ -z "$answer" ] part handles a last line without a trailing newline, such as printf 'y', where read reports EOF but still filled the variable.
Return codes are 0 for yes, 1 for no and 2 for "no answer" or bad usage. Since if only cares about zero versus non-zero, if yesno ... treats 2 as no, which is the safe choice for a confirmation prompt. If you need to tell the cases apart, check $?.
Usage
if yesno "Delete old backups?" n; then
printf 'Deleting...\n'
else
printf 'Keeping them.\n'
fi
yesno "Restart nginx?"
printf 'status: %s\n' "$?"Run from a terminal, pressing Enter at the first question, then typing maybe and Y at the second:
Delete old backups? [y/N]
Keeping them.
Restart nginx? [y/n] maybe
Please answer 'yes' or 'no'.
Restart nginx? [y/n] Y
status: 0Testing it with piped input
A prompt function is awkward to test by hand, so I fed it with printf instead. This small helper pipes its first argument into yesno and prints the return code:
t() { printf "$1" | yesno "${@:2}"; echo "-> $?"; }
t 'yes\n' "Continue?"
t 'YES\n' "Continue?"
t 'No\n' "Continue?"
t ' y \n' "Continue?"
t 'maybe\nsure\nY\n' "Continue?"
t '\n' "Continue?" y
t '' "Continue?" y
t '' "Continue?"
t 'maybe\n' "Continue?"
t 'y' "Continue?"-> 0
-> 0
-> 1
-> 0
Please answer 'yes' or 'no'.
Please answer 'yes' or 'no'.
-> 0
-> 0
-> 0
-> 2
Please answer 'yes' or 'no'.
-> 2
-> 0Two invalid answers are rejected before Y is accepted. An empty line and an empty pipe both fall back to the default. With no default, an empty pipe returns 2 immediately, and so does a single invalid answer followed by EOF. Notice that no prompts appear in the output: Bash only displays the -p prompt when input is coming from a terminal.
Pitfalls
Never loop on EOF
This is the bug in the 2009 version. Its loop kept calling read until the answer was exactly yes or no, without checking whether read had failed. When stdin is a pipe or /dev/null (cron, a CI job, ssh host script < /dev/null), every read fails instantly with an empty answer and the loop runs forever, silently, at 100% CPU. I had to stop it with timeout:
$ timeout 2 bash -c '. ./original.sh; yesno "Continue?" < /dev/null'
$ echo "exit: $?"
exit: 124Exit status 124 means timeout killed it, and wrapping the same command in time showed 1.96 seconds of user CPU in those 2 seconds. Piping Y into the original does the same, because it only accepted lowercase yes and no. Even typing y at a terminal was refused. On a terminal, pressing Ctrl+D in the original simply printed "Please enter 'yes' or 'no'" again, leaving Ctrl+C as the only way out.
The prompt needs a terminal, the answer does not
If your script reads data from stdin (tar cf - . | ./myscript) and also asks a question, yesno will take a line of that data as its answer. In that case read the answer from the terminal explicitly by changing the read line to read -r -p "..." answer < /dev/tty. Only do that when you know a terminal exists. Without a controlling terminal (cron, for example) the redirection fails with /dev/tty: No such device or address and read fails, which this function then treats as EOF.
Prompt goes to stderr
read -p writes the prompt to standard error, and so do the "Please answer" messages here. That is deliberate: ./script.sh > out.log still shows the question on screen and keeps the log clean.
set -u and set -e
Every variable is initialised, so the function is safe under set -u. Under set -e, a bare yesno "..." that returns 1 will end your script. Call it inside if, while or with ||, as you should with any function that returns a status.
Alternatives
For a single-key answer without Enter, use read -r -n 1 -p "..." answer and then printf '\n'. It feels quicker but is easy to trigger by accident, so I keep the line-based version for anything destructive.
Non-interactive scripts should not prompt at all. The usual pattern is a -y option that skips the question, the way apt-get -y and dnf -y do. See the getopts post for parsing it. For the read options themselves, the Bash manual entry for read is short and worth a look.
History of this snippet
The 2009 version accepted only lowercase yes and no, returned 1 (a "no") on bad usage, had no default and looped forever on EOF. Its own usage example was inverted as well: while ! yesno "..."; do echo "yes typed"; done prints "yes typed" every time the user answers no, and stops when they answer yes. Piping no and then yes into it prints "yes typed" exactly once, after the "no". The version above fixes all of that and keeps the original name and return convention.