Asking a Yes/No Question from a Bash Script
June 26th, 2009 by Mitch Frazier in
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.
__________________________Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.
| Attachment | Size |
|---|---|
| shooting-yourself-in-the-foot.jpg | 19.86 KB |
Special Magazine Offer -- Free Gift with Subscription
Receive a free digital copy of Linux Journal's System Administration Special Edition as well as instant online access to current and past issues. CLICK HERE for offer
Linux Journal: delivering readers the advice and inspiration they need to get the most out of their Linux systems since 1994.
Subscribe now!
The Latest
Newsletter
Tech Tip Videos
- Nov-04-09
- Oct-29-09
- Oct-26-09
Recently Popular
From the Magazine
December 2009, #188
If last month's Infrastrucuture issue was too "big" for you then try on this month's Embedded issue. Find out how to use Player for programming mobile robots, build a humidity controller for your root cellar, find out how to reduce the boot time of your embedded system, and if you're new to embedded systems find out the basics that go into one. You can also read about the Beagle Board, the Mesh Potato and a spate of other interestingly named items. And along with our regular columns don't miss our new monthly column: Economy Size Geek.
Delicious
Digg
StumbleUpon
Reddit
Facebook








holy cow
On June 30th, 2009 imanoob (not verified) says:
I just read all of this. I am so new to bash, I don't even know what "shift" means, or why you use [[]] double brackets. I really appreciate that you wrote the article with noobs like me in mind, just reading it was fun, even if I barely understand it, I can copy/paste and use it. As for the comments, this is why Linux is great. we all get to have our very own way of doing it.
Thank you
I'd like to offer some humble improvements.
On June 29th, 2009 Steven W. Orr (not verified) says:
I have to warn you, I'm really not very much fun at parties, simply because I take my shell scripting seriously.
There's a bunch of bad stuff in your code so please don't take my criticism the wrong way. I am trying to be constructive.
#!/bin/bash # First thing to note is that this is a bash script. Your script seemed to be running something called bash.sh # I have no idea what that was. # Set up warning and error. Why NOT make error call warning. Also note that there is never a good reason to refer # to $*. Instead, always use "$@". And the cost savings of using >&2 instead of 1>&2 is just not worth it. warning() { echo "$@" 1>&2; } # Also, let's not forget that simple commands can be stacked into one command line. error() { warning "$@"; exit 1; } # Here we set usage up so that we can die with a bit more elegance. usage() { warning "$@" cat <&2 Usage: $prog [--timeout val] [--default yes|no] prompt EOF exit 1 } # # 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. # One notion is to declare things to be of type int. # Also, never do your own options parsing. It's really hard and why invent the wheel? yesno() { local ans local -i ok=0 local -i timeout=0 local tv # TimeoutVal local default local temp # BTW, NEVER give a variabel a one-letter name. It's hard to find them using any editor. temp=$(getopt -o :d:t:h --long default:,timeout:,help -n $prog -- "$@") eval set -- "$temp" # The double quotes are key here. shopt -s nocasematch # Hey, this is bash. Let's not make life harder for ourselves. # None of this checking to see if it's equal to y or to Y. while true do case "$1" in -d | --default) # No need to shift 1 to then get the value and then shift again. Just ref "$2" and then shift 2. default="$2" [[ -z "$default" ]] && error 'Missing default value' [[ "$default" != y && "$default" != yes && "$default" != n && "$default" != no ]] && error "Illegal default answer: $default" shift 2 ;; -t | --timeout) # Note that tv is a string but if we decide that it's safe to assign to an in var then we do it. tv="$2" [[ -z "$tv" ]] && error 'Missing timeout value' [[ "$tv" =~ ^[0-9][0-9]*$ ]] && timeout=$((tv)) || error "Illegal timeout value: $tv" shift 2 ;; -h | --help) usage "Here's what you need to do to run this" ;; --) This is how e get out of the options processing game. shift break ;; *) usage "$1: Unknown option" ;; esac done # Now that we're done with the procopt then we can do the consistency checks. if (( timeout != 0 )) && [[ -z "$default" ]] then error 'Non-zero timeout requires a default answer' fi (( $# == 0 )) && error 'Missing question' while (( ok == 0 )) do if (( timeout != 0 )) then if read -t $timeout -p "$@" ans then # Turn off timeout if answer entered. timeout=0 # No need for all this testing and then assigning stuff. Just do it. : ${ans:="$default"} else ans=$default fi else read -p "$@" ans : ${ans:=$default} fi [[ "$ans" = y || "$ans" = yes || "$ans" = n || "$ans" = no ]] && ok=1 || warning 'Valid answers are: yes y no n' done [[ "$ans" = y || "$ans" = yes ]] } prog=$0 # This is just so you can see that you don't need basename and dirname. This is bash, not sh. fn=${0##*/} noshext=${fn%.sh} # Also, not that I'm testing yesno, not 'yesno' and not "yesno". Don't interpolate if you don't have to.. if [[ ${noshext} == yesno ]] then yesno 'Test bad timeout value? ' && yesno --timeout none "Hello? " yesno 'Test timeout without default value? ' && yesno --timeout 10 'Hello? ' yesno 'Test bad default value? ' && yesno --default none "Hello? " yesno 'Yes or no? ' && echo 'You answered yes' || echo 'You answered no' yesno --default yes 'Yes or no (default yes) ? ' && echo 'You answered yes' || echo "You answered no" yesno --default no 'Yes or no (default no) ? ' && echo 'You answered yes' || echo 'You answered no' yesno --timeout 5 --default no 'Yes or no (timeout 5, default no) ? ' && echo 'You answered yes' || echo 'You answered no' yesno --timeout 5 --default yes 'Yes or no (timeout 5, default yes) ? ' && echo 'You answered yes' || echo 'You answered no' fiSo, in the end, the goal is to write bash code and not Bourne shell code. Make it simple to read.
Some Useful Improvements
On June 30th, 2009 Mitch Frazier says:
There are some useful improvements in your code: the usage function, the single character flags, and compressing the test code so it's less obtrusive, I like that. And yeah, should be bash, not bash.sh in the first line. However, the rest of your changes and the "programming" recommendations in your comments, to me, are just style differences and they don't make your stuff "good" and mine "bad" as you say, they just make them different.
So, in the end, the goal is to write something that does what you want.
p.s. I suspect you're right, you're probably not much fun at parties.
__________________________Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.
What common mistake??
On June 29th, 2009 JohnH (not verified) says:
What common mistake does this script help to avoid? Your 'this common mistake' link is just a picture to a gun pointing at a foot.
Of Shooting Myself in the Foot
On June 29th, 2009 Mitch Frazier says:
The mistake of running something potentially "dangerous" without verifying that I really want to do it and thereby shooting myself in the foot.
__________________________Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.
Poor - horrendously complicated and unnecessary
On June 29th, 2009 JohnH (not verified) says:
That code is horrendously complicated for something that should be simple. For example, why test for 'yes' and 'no' when you could force a one-character user input by passing '-n 1' to the read command? Secondly, use getopt or getopts to parse arguments - gives more flexibility. Also why allow the user to make mistakes by allowing them to specifiy arbitrary defaults? Specifying the default by passing '-y' or '-n' means it's easier to use and easier to write (no need to check for invalid defaults - handled automatically by getopt).
Thanks For Remaining Calm
On June 29th, 2009 Mitch Frazier says:
Why not use "-n 1"? Well because it returns as soon as you type a character, so you don't get an opportunity to rethink your answer. Also, if you type more than one character in reply to the prompt, the subsequent characters get used by the next read command. Neither of those is desirable in my mind.
I don't usually use getopt/getopts because I find the resulting code less clear and often just as long as hand written code.
The -y/-n is a good idea.
__________________________Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.
Nice.
On June 28th, 2009 b2bwild (not verified) says:
Nice, but bash is so confusing. I'll stick to python.
really...
On June 28th, 2009 jnee says:
Why often this judgemental tone? "I'm better than you , and anonymous as well" .
Well, live and let live. Be positive and help people grow, much better. I'm new at this (bash scripts) and it's better to have almost perfect info that I understand than "Wow look at me" info that I don't understand.
overly verbose test
On June 27th, 2009 anonymous (not verified) says:
The following:
if [[ "$t" != 'y' && "$t" != 'yes' && "$t" != 'n' && "$t" != 'no' ]]; then
error "Illegal default answer: $default"
fi
Can be more clearly put as:
case "${t}" in
y)
yes)
n)
no) ;;# acceptable
*)
error "Illegal default answer: $default
esac
Even Shorter
On June 27th, 2009 Mitch Frazier says:
An even more compact version of your version would be:
case "${t}" in y|yes|n|no) ;; # acceptable *) error "Illegal default answer: $default";; esacSince your version contains fewer characters I guess that makes it less verbose, but personally I also find it to be less clear.
__________________________Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.
Your use of $* over $@ shows
On June 27th, 2009 Anonymous (not verified) says:
Your use of $* over $@ shows that you have still many bullets left.
Your use of the first few words of your comment as the
On June 29th, 2009 Anonymous (not verified) says:
Your use of the first few words of your comment as the Subject of your comments shows a lack of imagination and a poor understanding of the purpose of the Subject field.
Yes Grasshopper
On June 27th, 2009 Mitch Frazier says:
Many bullets...
__________________________Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.
I prefer a simpler solution...
On June 26th, 2009 wom (not verified) says:
until [ $happy = "Yes"]; do
echo "do you like pie? Your stuck in here until then."
select happy in Yes No
do
break
done
Gives you a simple "press 1 for yes 2 for no" menu to use; fudge for you own use of course.
I like this!!
On June 30th, 2009 טכנאי מחשבים (not verified) says:
I like this simpler solution as well! :-)
Post new comment