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.
| Attachment | Size |
|---|---|
| shooting-yourself-in-the-foot.jpg | 19.86 KB |
Mitch Frazier is an Associate Editor for Linux Journal.
Realizing the promise of Apache® Hadoop® requires the effective deployment of compute, memory, storage and networking to achieve optimal results. With its flexibility and multitude of options, it is easy to over or under provision the server infrastructure, resulting in poor performance and high TCO. Join us for an in depth, technical discussion with industry experts from leading Hadoop and server companies who will provide insights into the key considerations for designing and deploying an optimal Hadoop cluster.
Sponsored by AMD
If you already use virtualized infrastructure, you are well on your way to leveraging the power of the cloud. Virtualization offers the promise of limitless resources, but how do you manage that scalability when your DevOps team doesn’t scale? In today’s hypercompetitive markets, fast results can make a difference between leading the pack vs. obsolescence. Organizations need more benefits from cloud computing than just raw resources. They need agility, flexibility, convenience, ROI, and control.
Stackato private Platform-as-a-Service technology from ActiveState extends your private cloud infrastructure by creating a private PaaS to provide on-demand availability, flexibility, control, and ultimately, faster time-to-market for your enterprise.
Sponsored by ActiveState
| Non-Linux FOSS: libnotify, OS X Style | Jun 18, 2013 |
| Containers—Not Virtual Machines—Are the Future Cloud | Jun 17, 2013 |
| Lock-Free Multi-Producer Multi-Consumer Queue on Ring Buffer | Jun 12, 2013 |
| Weechat, Irssi's Little Brother | Jun 11, 2013 |
| One Tail Just Isn't Enough | Jun 07, 2013 |
| Introduction to MapReduce with Hadoop on Linux | Jun 05, 2013 |
- Containers—Not Virtual Machines—Are the Future Cloud
- Non-Linux FOSS: libnotify, OS X Style
- New Products
- Validate an E-Mail Address with PHP, the Right Way
- RSS Feeds
- Introduction to MapReduce with Hadoop on Linux
- Lock-Free Multi-Producer Multi-Consumer Queue on Ring Buffer
- Help with Designing or Debugging CORBA Applications
- Returning Values from Bash Functions
- Linux Systems Administrator
- Welcome to 1998
12 min 50 sec ago - notifier shortcomings
36 min 32 sec ago - heroku?
2 hours 13 min ago - Android User
2 hours 15 min ago - Reply to comment | Linux Journal
4 hours 8 min ago - compiling
6 hours 57 min ago - This is a good post. This
12 hours 10 min ago - Great, This is really amazing
12 hours 12 min ago - These posts are really good
12 hours 14 min ago - It’s a really great site you
12 hours 16 min ago
Featured Jobs
| Linux Systems Administrator | Houston and Austin, Texas | Host Gator |
| Senior Perl Developer | Austin, Texas | Host Gator |
| Technical Support Rep | Houston and Austin, Texas | Host Gator |
| UX Designer | Austin, Texas | Host Gator |
| Web & UI Developer (JavaScript & j Query) | Austin, Texas | Host Gator |
Free Webinar: Hadoop
How to Build an Optimal Hadoop Cluster to Store and Maintain Unlimited Amounts of Data Using Microservers
Realizing the promise of Apache® Hadoop® requires the effective deployment of compute, memory, storage and networking to achieve optimal results. With its flexibility and multitude of options, it is easy to over or under provision the server infrastructure, resulting in poor performance and high TCO. Join us for an in depth, technical discussion with industry experts from leading Hadoop and server companies who will provide insights into the key considerations for designing and deploying an optimal Hadoop cluster.
Some of key questions to be discussed are:
- What is the “typical” Hadoop cluster and what should be installed on the different machine types?
- Why should you consider the typical workload patterns when making your hardware decisions?
- Are all microservers created equal for Hadoop deployments?
- How do I plan for expansion if I require more compute, memory, storage or networking?



Comments
Even easier method
I discovered an even easier method to ask y/n (with no timeout):
echo "Would you like to 'x' [y/n]?"
read ans
if [ $ans = y -o $ans = Y -o $ans = yes -o $ans = Yes -o $ans = YES ]
then
echo "Place the 'yes' action here."
fi
if [ $ans = n -o $ans = N -o $ans = no -o $ans = No -o $ans = NO ]
then
echo "Place the 'no' action here."
fi
Notes: I did not write all of this from scratch. I discovered this method by opening the installer.sh of the Mac4Lin theme/project. I did, however, add the part that allows a 'no' action, as well as adding the full words of 'yes' and 'no', instead of just 'y' or 'n'. Also, if you want to use multiple questions in a single sh file, it seems that reusing the $ans variable does not cause any problems. The final thing is that, as noted above, this does not have a timeout. If you can figure out how to add a timeout, then COOL!, but I don't know how personally, as I don't know much about Linux. Btw, I am running Ubuntu Lucid Lynx (10.04).
Let's go og!
Although I like the added functionality of the original example (and pretty much found that the scary rewrite from mr-life-of-the-party was roughly equivalent to developing a fly-killing tank from scratch, starting with the wheel and ending with targeting systems), I have to say that, for my money, I like to go og (although, of course, much better with the tolower equiv built into the orig solution as detailed in the article):
read -p "Disco? (y/n): " ans #gets user input, assigns it to ans
case $ans in #what did monkey boy say to mr program?
y | yes) #was it y or yes? If so...
echo "You have chosen disco. I will summon disco immediately!"
;;
n | no)
echo "The master does not require disco?! I will summon a menu of other things!" #or just exit, or what have you
# function call for menu_of_other_things
;;
*) #perhaps the master doesn't understand the question...
echo "MASTER: DISCO: what part of y\n do you no longer understand? Exiting..."
echo "...Proud, but dejected, our yes or no menu exits, walking into the sunset, disco-less, but certain in the knowledge that he was easier to write than any of the other examples (and basically designed for total noobs... but, if you don't need a tank to kill a fly, a swatter works just fine"
;;
exit
esac
Sorry, but couldn't resist! Last I checked, bash scripts should work as simply as possible and be incredibly easy to read. Unless it's being passed to an end user who has no idea what the program is supposed to do (and, even then, only if the above silly little yes/no were capable of controlling something crucial and dangerous), why spend forever writing yes no in the most complicated way possible? Still, the actual article was very useful... I don't understand the open subshells (which look like C-ish things to me...) at the end of the function calls before the curly brackets... I would have spent more time describing esoteric aspects of the shell like that... I have enough problems figuring out how to get the right quotes in regex when working with awk in my scripts... Just saying, though. Still! Thanks!
Cheers!
PS: for those new to bash:
read -p "Is it me, or do I look invisible to you? (y/n): " var
case $var in
y | Y | yes | YES | Yes) #does the value of var match any of these?
echo "You answered yes. Perform actions until the double semicolons."
echo "Linux Journal rocks."
echo "Can't believe how complicated they made this simple structure!"
echo "Disco is frightening..."
;; # notes the end of that option in the list
n | N | no |NO | No)
echo "You said no."
;;
*) #I am all other input
echo "You pressed a random series of keys in excitement! HAHAHAA!!!"
;;
esac #ends the case list
holy cow
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.
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
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.
What common mistake??
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
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.
Poor - horrendously complicated and unnecessary
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
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.
Nice.
Nice, but bash is so confusing. I'll stick to python.
really...
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
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
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.
Your use of $* over $@ shows
Your use of $* over $@ shows that you have still many bullets left.
Your use of the first few words of your comment as the
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
Many bullets...
Mitch Frazier is an Associate Editor for Linux Journal.
I prefer a simpler solution...
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!!
I like this simpler solution as well! :-)