1

In my bash scripts, I often prompt users for y/n answers. Since I often use this several times in a single script, I'd like to have a function that checks if the user input is some variant of Yes / No, and then cleans this answer to "y" or "n". Something like this:

yesno(){
    temp=""
    if [[ "$1" =~ ^([Yy](es|ES)?|[Nn][Oo]?)$ ]] ; then
        temp=$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/es//g' | sed 's/no//g')
        break
    else
        echo "$1 is not a valid answer."
    fi
}

I then would like to use the function as follows:

while read -p "Do you want to do this? " confirm; do # Here the user types "YES"
    yesno $confirm
done
if [[ $confirm == "y" ]]; then
    [do something]
fi

Basically, I want to change the value of the first argument to the value of $confirm, so that when I exit the yesno function, $confirm is either "y" or "n".

I tried using set -- "$temp" within the yesnofunction, but I can't get it to work.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • This seems very much a X-Y question -- where the question is about a proposed solution, as opposed to the problem the OP actually wants to use that solution to resolve. The accepted answer doesn't change `"$1"` in the outer scope at all, so other people finding it as a search result are likely to be confused -- perhaps the question should be edited to more closely describe the real/immediate problem? – Charles Duffy Dec 20 '17 at 22:03
  • ...which is to say, based on the original title, I would have thought the OP wanted to know how to change `$1` itself, not the value of a variable *named in the contents of* `$1`. – Charles Duffy Dec 20 '17 at 22:17

2 Answers2

2

You could do it by outputting the new value and overwriting the variable in the caller.

yesno() {
    if [[ "$1" =~ ^([Yy](es|ES)?|[Nn][Oo]?)$ ]] ; then
        local answer=${1,,}
        echo "${answer::1}"
    else
        echo "$1 is not a valid answer." >&2
        echo "$1"  # output the original value
        return 1   # indicate failure in case the caller cares
    fi
}

confirm=$(yesno "$confirm")

However, I'd recommend a more direct approach: have the function do the prompting and looping. Move all of that repeated logic inside. Then the call site is super simple.

confirm() {
    local prompt=$1
    local reply

    while true; do
        read -p "$prompt" reply

        case ${reply,,} in
            y*) return 0;;
            n*) return 1;;
            *)  echo "$reply is not a valid answer." >&2;;
        esac
    done
}

if confirm "Do you want to do this? "; then
    # Do it.
else
    # Don't do it.
fi

(${reply,,} is a bash-ism that converts $reply to lowercase.)

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
1

You could use the nameref attribute of Bash (requires Bash 4.3 or newer) as follows:

#!/bin/bash

yesno () {
    # Declare arg as reference to argument provided
    declare -n arg=$1

    local re1='(y)(es)?'
    local re2='(n)o?'

    # Set to empty and return if no regex matches
    [[ ${arg,,} =~ $re1 ]] || [[ ${arg,,} =~ $re2 ]] || { arg= && return; }

    # Assign "y" or "n" to reference
    arg=${BASH_REMATCH[1]}
}

while read -p "Prompt: " confirm; do
    yesno confirm
    echo "$confirm"
done

A sample test run looks like this:

Prompt: YES
y
Prompt: nOoOoOo
n
Prompt: abc

Prompt: 

The expressions are anchored at the start, so yessss etc. all count as well. If this is not desired, an end anchor ($) can be added.

If neither expression matches, the string is set to empty.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116