Useful Bash functions - part 2 (HPR Show 2096)

Dave Morriss


Table of Contents

Overview

This is the second show about Bash functions. In this one I revisit the yes_no function from the last episode and deal with some of the deficiencies of that version.

As before it would be interesting to receive feedback on these versions of the function and would be great if other Bash users contributed ideas of their own.

The yes_no function revisited

In the last episode (1757, released 28 April 2015) where I demonstrated some Bash functions I use, I talked about my yes_no function. It is called with a question in the form of a prompt string and an optional default answer and returns a Bash true or false result so that it can be used when making choices in scripts.

When run, the version I talked about placed the default value on the line after the prompt. This had to be deleted if the user did not want to accept the default, and feedback showed that this was probably a poor design.

Since then I have redesigned this function. I have two versions which I am talking about in this episode.

The mark 2 version

As before in episode 1757 this is a function that can be used to return a true/false result in an if statement. The main differences are that it can generate part of the prompt automatically, and it doesn’t show the default on the command line.

So, invoking it thus:

if ! yes_no_mk2 'Do you want to continue? %s ' 'N'; then
    return
fi

results in the prompt:

Do you want to continue? [y/N]

The function has replaced %s with the string [y/N] which is a convention you will often see in command line tools. The square brackets hold the two possible responses with the default one being capitalised. So, this one shows the responses should be ‘y’ or ‘n’ but that if nothing is typed it is taken as being ‘n’.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#===  FUNCTION  ================================================================
#         NAME: yes_no_mk2
#  DESCRIPTION: Read a Yes or No response from STDIN and return a suitable
#               numeric value
#   PARAMETERS: 1 - Prompt string for the read
#               2 - Default value (optional)
#      RETURNS: 0 for a response of Y or YES, 1 otherwise
#===============================================================================
yes_no_mk2 () {
    local prompt="${1:?Usage: yes_no prompt [default]}"
    local default="${2^^}"
    local ans res

    if [[ $prompt =~ %s ]]; then
        if [[ -n $default ]]; then
            default=${default:0:1}
            case "$default" in
                Y) printf -v prompt "$prompt" "[Y/n]" ;;
                N) printf -v prompt "$prompt" "[y/N]" ;;
                *) echo "Error: ${FUNCNAME[0]}: Line ${BASH_LINENO[0]}: Default must be 'Y' or 'N'"
                   exit 1
                   ;;
            esac
        else
            echo "Error: ${FUNCNAME[0]}: Line ${BASH_LINENO[0]}: Default required"
            exit 1
        fi
    fi

    #
    # Read and handle CTRL-D (EOF)
    #
    read -e -p "$prompt" ans
    res="$?"
    if [[ $res -ne 0 ]]; then
        echo "Read aborted"
        return 1
    fi

    [ -z "$ans" ] && ans="$default"
    if [[ ${ans^^} =~ ^Y(E|ES)?$ ]]; then
        return 0
    else
        return 1
    fi
}
  • Lines 10-12 define variables to hold the arguments and other things. Note that line 11 ensures the default (which should be ‘y’ or ‘n’) is in upper case.
  • Lines 14-28 deal with the prompt string.
    • The presence of ‘%s’ is checked on line 14 using a Bash regular expression.
    • If this is present then there needs to be a default, and the test on line 15 checks whether the default variable is non-empty using the -n operator.
    • If it is not empty then just the first character is taken on line 16.
    • Lines 17-23 are a case statement that takes specific action based on the contents of the default variable.
      • If the contents are ‘Y’ then the string ‘[Y/n]’ is substituted into the prompt using a printf command.
      • An ‘N’ produces ‘[y/N]’ the same way.
      • If it is neither then the function generates an error message and exits the entire script. Note that ‘${FUNCNAME[0]}’ returns the name of the function in a general way, and ‘${BASH_LINENO[0]}’ contains the current line number within the Bash script.
    • Lines 25-26 deal with the case where the default variable is empty. That’s an error because the prompt variable contains the %s substitution point, so the function aborts.
  • Line 33 performs the read with the prompt, collecting what was typed in variable ans.
  • Line 34 saves the result from the read in case it was aborted with CTRL-D.
  • Lines 35-38 will cause the script to return a false result if CTRL-D was pressed.
  • Line 40 replaces ans with the default value if it is empty.
  • Lines 41-45 check the upper case version of ans, comparing it using a regular expression to see if it is ‘Y’, ‘YE’ or ‘YES’. If it is true is returned, and if it isn’t then the function returns false.

The mark 3 version

The problem with the mark 2 version is that it treats an answer that is not ‘Y’ as if it is ‘N’. I developed mark 3 to compensate for this. It is essentially the same except that it looks for YES and NO (and shorter forms) and rejects anything else.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#===  FUNCTION  ================================================================
#         NAME: yes_no_mk3
#  DESCRIPTION: Read a Yes or No response from STDIN (only these values are
#               accepted) and return a suitable numeric value.
#   PARAMETERS: 1 - Prompt string for the read
#               2 - Default value (optional)
#      RETURNS: 0 for a response of Y or YES, 1 otherwise
#===============================================================================
yes_no_mk3 () {
    local prompt="${1:?Usage: yes_no prompt [default]}"
    local default="${2^^}"
    local ans res

    if [[ $prompt =~ %s ]]; then
        if [[ -n $default ]]; then
            default=${default:0:1}
            case "$default" in
                Y) printf -v prompt "$prompt" "[Y/n]" ;;
                N) printf -v prompt "$prompt" "[y/N]" ;;
                *) echo "Error: ${FUNCNAME[0]} @ line ${BASH_LINENO[0]}: Default must be 'Y' or 'N'"
                   exit 1
                   ;;
            esac
        else
            echo "Error: ${FUNCNAME[0]} @ line ${BASH_LINENO[0]}: Default required"
            exit 1
        fi
    fi

    #
    # Loop until a valid input is received
    #
    while true; do
        #
        # Read and handle CTRL-D (EOF)
        #
        read -e -p "$prompt" ans
        res="$?"
        if [[ $res -ne 0 ]]; then
            echo "Read aborted"
            return 1
        fi

        [ -z "$ans" ] && ans="$default"

        #
        # Look for valid replies and return appropriate values. Print an error
        # message otherwise and loop around for another go
        #
        if [[ ${ans^^} =~ ^Y(E|ES)?$ ]]; then
            return 0
        elif [[ ${ans^^} =~ ^NO?$ ]]; then
            return 1
        else
            echo "Invalid reply; please use 'Y' or 'N'"
        fi
    done
}
  • Lines 1-32 are the same as in the mark 2 version
  • Line 33 begins a while loop. This uses the built-in true command which just returns a true value. What this combination produces is an infinite loop. The loop ends at line 57.
  • Lines 37-42 print a prompt and read a response in the same way as lines 33-38 in the mark 2 version. Pressing CTRL-D in response to the prompt is detected here and the loop and the function are exited with a false value (1).
  • Line 44 checks to see if anything was typed and if not supplies the default. This is the same as line 40 in the mark 2 version.
  • Lines 50-56 are where the rest of the changes have been made.
    • Lines 50 and 51 test the upper case version of the returned response against a regular expression accepting ‘Y’, ‘YE’ or ‘YES’. If it matches then the loop and the function are exited with a true value (0).
    • If the first test does not match the test at line 52 is applied. This tests the upper case version of the returned response against a regular expression accepting ‘N’ or ‘NO’. If it matches then the loop and the function are exited with a false value (1).
    • If neither of the earlier tests matched then an error message is displayed at line 55, and the loop will then repeat the prompt and the tests.

A typical use of this function in a script might be:

if ! yes_no_mk3 'Do you want to continue? %s ' 'N'; then
    echo "Finished"
    return
fi

This might result in the following dialogue:

Do you want to continue? [y/N] what
Invalid reply; please use 'Y' or 'N'
Do you want to continue? [y/N] yo
Invalid reply; please use 'Y' or 'N'
Do you want to continue? [y/N] nope
Invalid reply; please use 'Y' or 'N'
Do you want to continue? [y/N] no
Finished

I’d say that the mark 3 version is more useful overall, and this is the one I shall be adopting myself. A copy of the mark 3 version of the function can be downloaded from here (with the name yes_no).

If you have any further additions to this function or comments about it then please let me know.