3
\$\begingroup\$

I've written a tiny function for asking questions intended for my POSIX shell scripts, where I often need user input.

The function takes 2+ arguments, where:

  1. Is a string containing the question.
  2. , 3., ... = arguments containing the right answers (case insensitive)

I also changed my style by a bit, no longer use ${var} instead of just $var.

The requirement was simple: Check for exactly the answers given. So, not matching yeahh if given answer is yeah.

I also included a maybe performance-wise quick test if the user answer is in the list of answers, so answering no will not make the script iterate through all the answers and just dies at that check.


#!/bin/sh
set -u
confirmation ()
# $1    = a string containing the question
# $2,.. = arguments containing the right answers (case insensitive)
{
    question=$1; shift
    correct_answers=$*
    correct_answers_combined=$( printf '%s' "$correct_answers" | sed 's/\( \)\{1,\}/\//g' )
    printf '%b' "$question\\nPlease answer [ $correct_answers_combined ] to confirm (Not <Enter>): "
    read -r user_answer

    # this part is optional in hope it would speed up the whole process
    printf '%s' "$correct_answers" | grep -i "$user_answer" > /dev/null 2>&1 ||
        return 1

    # this part iterates through the list of correct answers
    # and compares each as the whole word (actually as the whole line) with the user answer
    for single_correct_answer in $correct_answers; do
        printf '%s' "$single_correct_answer" | grep -i -x "$user_answer" > /dev/null 2>&1 &&
            return 0
    done

    # this might be omitted, needs verification, or testing
    return 1
}

# EXAMPLE usage, can be anything, DO NOT review this part please
if confirmation 'Is dog your favorite pet?' y yes yep yeah
then
    tput bold; tput setaf 2; echo 'TRUE: You just love dogs! :)'; tput sgr0
else
    tput bold; tput setaf 1; echo 'FALSE: Dog hater, discontinuing! :('; tput sgr0
    exit 1
fi
# do other stuff here in TRUE case
echo 'And here comes more fun...'
\$\endgroup\$
  • \$\begingroup\$ What if I answer .*? That's not in the list of accepted answers, yet it is accepted. \$\endgroup\$ – Roland Illig May 12 at 4:46
  • \$\begingroup\$ What if one of the valid answers contains a space? \$\endgroup\$ – Roland Illig May 12 at 4:48
  • \$\begingroup\$ @RolandIllig it can't contain space, I didn't think of .*, thanks \$\endgroup\$ – LinuxSecurityFreak May 12 at 4:55
3
\$\begingroup\$

printf '%b' will expand backslash escapes in $question and in $correct_answers_combined. It's not obvious that both of those are desirable.

I'd probably re-write that to expand only $question, and to avoid an unnecessary pipeline:

printf '%b\nPlease answer [ ' "$question"
printf '%s ' "$@"
printf '] to confirm (Not <Enter>): '

You almost certainly want fgrep (or grep -F) rather than standard regular-expression grep, and it would be simpler to search through the items one per line, rather than using a for loop:

read -r user_answer

printf '%s\n' "$@" | grep -qFx "$user_answer"

If this is the last command in the function, then the return status will be that of the grep command, which is just what we need.

Finally, be aware that read can fail (e.g. when it reaches EOF). If you don't want that to be an automatic "no", then make provision for that. I don't know what the right behaviour is for this application so I'll leave that as an open issue for you to address appropriately.


Modified version

Here's what I ended up with:

# $1    = a string containing the question
# $2,.. = arguments containing the right answers (case insensitive)
confirmation()
{
    question=$1; shift

    printf '%b\nPlease answer [ ' "$question"
    printf '%s ' "$@"
    printf '] to confirm (Not <Enter>): '
    read -r user_answer

    printf '%s\n' "$@" | grep -qFxi "$user_answer"
}
\$\endgroup\$
3
\$\begingroup\$

A couple of thoughts. There really isn't any need for correct_answers_combined. After shift, $* will hold the combined remaining arguments (answers) to your question. The additional printf, pipe, and call to sed are simply incurring additional overhead in the form of subshells and separate utility calls. You could do:

    correct_answers="$*"

    ## prompt
    printf '%b' "$question\\nPlease answer [ $correct_answers ] to confirm (Not <Enter>): "
    read -r user_answer

Since you do not want to accept [Enter] as an answer, a validation that user_answer is unset can provide the answer and return for your function in that case, e.g.

    ## validate answer provided
    [ -z "$user_answer" ] && return 1

Your "optional... speed up the whole process", with a bit of rearranging can be used as a single call to provide a return to your function. Since you want to know whether the user_answer exists among the correct_answers you can simply return the result of grep -qi:

    # this part is can be the whole process
    printf '%s\n' $correct_answers | grep -qi "$user_answer" > /dev/null 2>&1
        return $?
}

(note: using printf '%s\n' "$correct_answers" will separate each of the whitespace separated answers by newline which eliminates any possible combination of parts of adjacent answers returning true)

With those suggestions, your confirmation () function would reduce to:

confirmation ()
# $1    = a string containing the question
# $2,.. = arguments containing the right answers (case insensitive)
{
    question="$1"; shift
    correct_answers="$*"

    ## prompt
    printf '%b' "$question\\nPlease answer [ $correct_answers ] to confirm (Not <Enter>): "
    read -r user_answer

    ## validate answer provided
    [ -z "$user_answer" ] && return 1

    # this part is can be the whole process
    printf '%s\n' "$correct_answers" | grep -qi "$user_answer" > /dev/null 2>&1
        return $?
}

I haven't tested all corner cases, but for single word answers it should function as you intend. If you want the some delimiter between the answers inside [ ... ] then an additional command substitution can be used, but space separated options appear fine between the brackets.

Let me know if you have any questions over the changes.

Edit In Response to You Comments

As stated above, if you want delimiters between the possible correct answers, simply use a command substitution, e.g.

## prompt
printf '%b' "$question\\nPlease answer [ $(echo $correct_answers | tr ' ' /) ] to confirm (Not <Enter>): "
read -r user_answer

If you are concerned about the expansion of printf '%s\n', don't quote the $correct answers,

# this part is can be the whole process
printf '%s\n' $correct_answers | grep -qi "$user_answer" > /dev/null 2>&1
    return $?

Otherwise based on your stated question, it performs identical to the original saving at least half-a-dozen unnecessary subshells.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.