# POSIX shell function for asking questions

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>): "

# 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...'  • What if I answer .*? That's not in the list of accepted answers, yet it is accepted. – Roland Illig May 12 '19 at 4:46 • What if one of the valid answers contains a space? – Roland Illig May 12 '19 at 4:48 • @RolandIllig it can't contain space, I didn't think of .*, thanks – LinuxSecurityFreak May 12 '19 at 4:55 ## 2 Answers 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" }  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>): "

[ -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.