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.

2
  • 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? Commented Dec 20, 2017 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. Commented Dec 20, 2017 at 22:17

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

2 Comments

For future reference, it should be noted that ${reply,,} only works in Bash 4 or newer
@HubertLéveilléGauvin: Thanks for the hint. Any idea how to do it sensibly with bash 3?
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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.