Asking a Yes/No Question from a Bash Script

 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.

AttachmentSize
shooting-yourself-in-the-foot.jpg19.86 KB
______________________

Mitch Frazier is an Associate Editor for Linux Journal.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Even easier method

Ronson Wagner's picture

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!

skookiesprite's picture

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

imanoob's picture

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.

Steven W. Orr's picture

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'
fi

So, in the end, the goal is to write bash code and not Bourne shell code. Make it simple to read.

Some Useful Improvements

Mitch Frazier's picture

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??

JohnH's picture

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

Mitch Frazier's picture

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

JohnH's picture

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

Mitch Frazier's picture

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.

b2bwild's picture

Nice, but bash is so confusing. I'll stick to python.

really...

jnee's picture

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

anonymous's picture

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

Mitch Frazier's picture

An even more compact version of your version would be:

case "${t}" in
y|yes|n|no) ;; # acceptable
*)          error "Illegal default answer: $default";;
esac

Since 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

Anonymous's picture

Your use of $* over $@ shows that you have still many bullets left.

Your use of the first few words of your comment as the

Anonymous's picture

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

Mitch Frazier's picture

Many bullets...

Mitch Frazier is an Associate Editor for Linux Journal.

I prefer a simpler solution...

wom's picture

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!!

טכנאי מחשבים's picture

I like this simpler solution as well! :-)

White Paper
Linux Management with Red Hat Satellite: Measuring Business Impact and ROI

Linux has become a key foundation for supporting today's rapidly growing IT environments. Linux is being used to deploy business applications and databases, trading on its reputation as a low-cost operating environment. For many IT organizations, Linux is a mainstay for deploying Web servers and has evolved from handling basic file, print, and utility workloads to running mission-critical applications and databases, physically, virtually, and in the cloud. As Linux grows in importance in terms of value to the business, managing Linux environments to high standards of service quality — availability, security, and performance — becomes an essential requirement for business success.

Learn More

Sponsored by Red Hat

White Paper
Private PaaS for the Agile Enterprise

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.

Learn More

Sponsored by ActiveState