Bash

yesno() in Bash: Ask a Yes/No Question Safely

By Nawawi JamiliPublished Updated 6 min read

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

bash
# 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

bash
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:

output
Delete old backups? [y/N]
Keeping them.
Restart nginx? [y/n] maybe
Please answer 'yes' or 'no'.
Restart nginx? [y/n] Y
status: 0

Testing 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:

bash
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?"
output
-> 0
-> 0
-> 1
-> 0
Please answer 'yes' or 'no'.
Please answer 'yes' or 'no'.
-> 0
-> 0
-> 0
-> 2
Please answer 'yes' or 'no'.
-> 2
-> 0

Two 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:

terminal
$ timeout 2 bash -c '. ./original.sh; yesno "Continue?" < /dev/null'
$ echo "exit: $?"
exit: 124

Exit 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.