Asking a Yes/No Question from a Bash Script
In order to avoid this common mistake I often have my shell scripts prompt me for a yes or no answer before they proceed. The function described here is for doing that: asking a question and validating the answer.
The function is pretty simple it accepts a couple of options and the remainder of the arguments are presumed to be the question text. It prompts the user for an answer and validates that the answer is one of "yes", "y", "no", or "n". The answer is converted to lower case so any combination of case is accepted.
The available options are --timeout N which causes the prompt to timeout after N seconds, and --default ANS which provides a default answer for prompts that timeout. It also allows the user to hit ENTER and accept the default answer. The function code follows:
#!/bin/bash.sh
#
#####################################################################
# Print warning message.
function warning()
{
echo "$*" >&2
}
#####################################################################
# Print error message and exit.
function error()
{
echo "$*" >&2
exit 1
}
#####################################################################
# Ask yesno question.
#
# Usage: yesno OPTIONS QUESTION
#
# Options:
# --timeout N Timeout if no input seen in N seconds.
# --default ANS Use ANS as the default answer on timeout or
# if an empty answer is provided.
#
# Exit status is the answer.
function yesno()
{
local ans
local ok=0
local timeout=0
local default
local t
while [[ "$1" ]]
do
case "$1" in
--default)
shift
default=$1
if [[ ! "$default" ]]; then error "Missing default value"; fi
t=$(tr '[:upper:]' '[:lower:]' <<<$default)
if [[ "$t" != 'y' && "$t" != 'yes' && "$t" != 'n' && "$t" != 'no' ]]; then
error "Illegal default answer: $default"
fi
default=$t
shift
;;
--timeout)
shift
timeout=$1
if [[ ! "$timeout" ]]; then error "Missing timeout value"; fi
if [[ ! "$timeout" =~ ^[0-9][0-9]*$ ]]; then error "Illegal timeout value: $timeout"; fi
shift
;;
-*)
error "Unrecognized option: $1"
;;
*)
break
;;
esac
done
if [[ $timeout -ne 0 && ! "$default" ]]; then
error "Non-zero timeout requires a default answer"
fi
if [[ ! "$*" ]]; then error "Missing question"; fi
while [[ $ok -eq 0 ]]
do
if [[ $timeout -ne 0 ]]; then
if ! read -t $timeout -p "$*" ans; then
ans=$default
else
# Turn off timeout if answer entered.
timeout=0
if [[ ! "$ans" ]]; then ans=$default; fi
fi
else
read -p "$*" ans
if [[ ! "$ans" ]]; then
ans=$default
else
ans=$(tr '[:upper:]' '[:lower:]' <<<$ans)
fi
fi
if [[ "$ans" == 'y' || "$ans" == 'yes' || "$ans" == 'n' || "$ans" == 'no' ]]; then
ok=1
fi
if [[ $ok -eq 0 ]]; then warning "Valid answers are: yes y no n"; fi
done
[[ "$ans" = "y" || "$ans" == "yes" ]]
}
if [[ $(basename "$0" .sh) == 'yesno' ]]; then
if yesno "Test bad timeout value? "; then
yesno --timeout none "Hello? "
fi
if yesno "Test timeout without default value? "; then
yesno --timeout 10 "Hello? "
fi
if yesno "Test bad default value? "; then
yesno --default none "Hello? "
fi
if yesno "Yes or no? "; then
echo "You answered yes"
else
echo "You answered no"
fi
if yesno --default yes "Yes or no (default yes) ? "; then
echo "You answered yes"
else
echo "You answered no"
fi
if yesno --default no "Yes or no (default no) ? "; then
echo "You answered yes"
else
echo "You answered no"
fi
if yesno --timeout 5 --default no "Yes or no (timeout 5, default no) ? "; then
echo "You answered yes"
else
echo "You answered no"
fi
if yesno --timeout 5 --default yes "Yes or no (timeout 5, default yes) ? "; then
echo "You answered yes"
else
echo "You answered no"
fi
fi
# vim: tabstop=4: shiftwidth=4: noexpandtab:
# kate: tab-width 4; indent-width 4; replace-tabs false;
The code starts with a couple of functions which print warning and error messages. The main function checks the arguments then loops until it receives a valid answer. Note that if a timeout was specified and any answer (valid or invalid) is entered the timeout is turned off. The last line of the function tests the answer to see if it's value is "yes" or "y", thereby setting the exit status of the function.
The code at the end of the file is only executed if you invoke the file directly rather than sourceing it into your shell script. Output from a direct run follows:
$ sh yesno.sh
Test bad timeout value? n
Test timeout without default value? n
Test bad default value? n
Yes or no? yep
Valid answers are: yes y no n
Yes or no? yes
You answered yes
Yes or no (default yes) ? <ENTER>
You answered yes
Yes or no (default no) ? <ENTER>
You answered no
Yes or no (timeout 5, default no) ? You answered no
Yes or no (timeout 5, default yes) ? You answered yes
Notice the last couple of lines, no answer was provided so the default was used after the timeout. That's why the response text appears on the same line as the question.