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...'
200_success
145k22 gold badges190 silver badges478 bronze badges
asked May 12, 2019 at 4:37
\$\endgroup\$
3
  • \$\begingroup\$ What if I answer .*? That's not in the list of accepted answers, yet it is accepted. \$\endgroup\$ Commented May 12, 2019 at 4:46
  • \$\begingroup\$ What if one of the valid answers contains a space? \$\endgroup\$ Commented May 12, 2019 at 4:48
  • \$\begingroup\$ @RolandIllig it can't contain space, I didn't think of .*, thanks \$\endgroup\$ Commented May 12, 2019 at 4:55

2 Answers 2

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"
}
answered May 15, 2019 at 12:06
\$\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.

answered May 12, 2019 at 7:12
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.