Table of Contents
Overview
This is the third show about Bash functions. These are a little more advanced than in the earlier shows, and I thought I’d share them in case they are useful to anyone.
As before it would be interesting to receive feedback on these functions and would be great if other Bash users contributed ideas of their own.
Example Functions
The read_value
function
The purpose of this function is to output a prompt and read a string. The string is written to a nominated variable and a default string can be provided if required.
A typical call might be:
$ read_value 'What is your name? ' name
What is your name? Herbert
$ echo $name
Herbert
Here, the first argument is the prompt and the second is the name of the variable to receive the answer.
If the default is used then the reply is pre-filled with it:
$ read_value 'Where do you live? ' country USA
Where do you live? USA
This prompt can be deleted and another value used instead.
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 |
|
The function is not very complex and has a number of similarities with the various iterations of the yes_no
function encountered in earlier episodes. See the Links section below for links to these episodes.
We will not dwell too long on this function as a consequence of these similarities. However, it does something the previous functions didn’t do, it returns a string to the caller.
Bash functions can’t return anything but numeric values (via return
), unlike higher level languages. This function writes a global variable with the value it has requested. It could have been designed to always write to the same global variable, but this is ugly. I wanted the caller to be able to nominate the variable to receive the result.
The main purpose of the script is to call the Bash built-in command ‘read
’ with various options. If there is a default string provided we need to turn that into an option preceded by ‘-i
’. This is achieved on lines 18-20 where we store the result back in the variable ‘default
’.
The read
is performed on line 26, and we use the ‘eval
’ command to do this. This used because we need to make Bash scan the line twice, and the way eval
works allows us to achieve this.
The eval
command takes its arguments (in this case a string), concatenates them and executes the result in the current environment. The string it is presented with in this case is first processed by Bash where it performs the various types of expansion.
If we look at an example this might help to clarify the issue (the function has to be made available to Bash using the source
command before this will work):
$ read_value 'What is your surname? ' surname 'Not provided'
Here, the variable default
will contain ‘Not provided’ (without the quotes, which get stripped when the command is parsed by Bash), and that will be converted to “-i 'Not provided'
”. The argument to eval
will then be expanded to:
read -r -e -i 'Not provided' -p 'What is your surname? ' var
This command will then be executed.
If we had not used eval
and had instead written:
read -r -e $default -p "$prompt" var
The contents of default
would not have been parsed by Bash. Using eval
causes two scans of the command. First time the parameters are substituted, and the second time the command is executed.
The following is the result of running the function on the command line with tracing on (set -x
):
$ set -x
$ read_value 'What is your surname? ' surname 'Not provided'
+ read_value 'What is your surname? ' surname 'Not provided'
+ local 'prompt=What is your surname? '
+ local outputname=surname
+ local 'default=Not provided'
+ local var
+ [[ -n Not provided ]]
+ default='-i '\''Not provided'\'''
+ eval 'read -r -e -i '\''Not provided'\'' -p '\''What is your surname? '\'' var'
++ read -r -e -i 'Not provided' -p 'What is your surname? ' var
What is your surname? Putin
+ res=0
+ [[ 0 -ne 0 ]]
+ eval 'surname='\''Putin'\'''
++ surname=Putin
+ return 0
$ set +x; echo "$surname"
+ set +x
Putin
The lines that start with a $
are commands I typed, and those beginning with a +
are the output of Bash’s trace mode. I added blank lines after each command (plus any output it generated).
- You can see the function arguments being placed in local variables.
- The
default
value is saved - When the
eval
is shown all of the variables used are expanded, then the resulting command is run - The prompt is shown, with my response “Putin”
- The
eval
that returns the result in variablesurname
is shown (the variable name is in the local variablevar
) - I turn off the trace (
set +x
) and echo the result in this variable.
The check_value
function
This function was designed to be used in conjunction with read_value
to check that the string read in is valid.
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 |
|
The idea is that read_value
has been used to get a string from the user of a script. This string may need to be checked to see whether it conforms to a pattern. For example, I have written a script that helps me manage the HPR shows I am in the process of writing. At some point I will have chosen a slot in the queue and want to record that the show number is hpr9876
or whatever. I might want even to perform the check against a list of possibilities. I would give the incoming string to check_value
and get it to compare against a list of regular expressions.
- The function takes the value to be checked as the first argument followed by one or more (Bash-style) regular expressions. Note that we use a variant of the usual parameter substitution in the
local
command here1. - After saving the first argument in the variable called
value
the next thing the function does (line 15) is to drop the first argument from the parameter list. - Then (lines 16-19) it checks the
$#
variable (number of arguments) to see if there are any regular expressions. If not it prints an error message and exits with a ‘false’ value. - A
while
loop (lines 24-30) then processes each argument until there are no more. Each time it compares the argument (assuming it’s a regular expression) against the variable calledvalue
, incrementing thematches
variable if they match. After the testshift
is used to drop that argument. - By line 35 the
matches
variable should be zero if nothing matched or non-zero if there were any matches. The function returns ‘false’ in the first instance and ‘true’ otherwise.
This might be tested as follows:
$ source read_value.sh
$ source check_value.sh
$ demo () {
name=
until read_value "What is your first name? " name && test -n "$name"; do
:
done
if check_value "$name" "^[A-Za-z]+$" "^0[Xx][A-Fa-f0-9]+$"; then
echo "Hello $name"
else
echo "That name isn't valid"
fi
}
$ demo
What is your first name? Herbert
Hello Herbert
$ demo
What is your first name? 0x1101
Hello 0x1101
$ demo
What is your first name? Jim42
That name isn't valid
$ demo
What is your first name? 0xDEAD42
Hello 0xDEAD42
$ demo
What is your first name? DEAD42
That name isn't valid
- This defines a temporary function called
demo
- The variable
name
is created and made empty - The function
read_value
is called in anuntil
loop where it requests the caller’s first name and writes the response toname
. It is concatenated2 with a call to the built-intest
command using the-n
option against the contents of thename
variable (substituted into a string). This returns ‘true’ if the string is not empty, in which case the loop stops. If no name is given the loop will repeat. The loop body consists of a null command (colon). - A call to
check_value
follows in anif
command. If the check returns ‘true’ then thename
variable is echoed, otherwise an error message is given. - The call to
check_value
usesname
as the string to check and it matches against two regular expressions. The first checks for at least one upper or lower case alphabetic character, and nothing else (no spaces in the name allowed). The second expects a hexadecimal number which begins with ‘0X’ or ‘0x’ followed by at least one hexadecimal digit.
The read_and_check
function
This function uses the two previous functions to read a value and check it is valid. It loops until a valid reply is received or the process is aborted with CTRL-D.
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 |
|
- The function expects a prompt (for
read_value
), the name of a variable to receive the result, an optional default value and a list of regular expressions. - The first thing the function does is to call
read_value
(line 16) with the relevant parameters. It does this in anif
command because if the prompt is aborted with CTRL-D then a ‘false’ value is returned, so we abortread_and_check
if so. - Next, on line 19, the
shift
command deletes the first three arguments so we can pass the remainder tocheck_value
. - Then
check_value
is called in anuntil
loop (lines 20-26) checking the value returned against the list of regular expressions. If the check is passed the loop ends and the function exits. If not then an error message is written andread_value
called again (with a check for CTRL-D as before). - A complicating factor in this function is that the local variable
outputname
contains the name of the (global) variable to receive the value from the user. When we want to examine the contents of this variable we have to use indirect expansion of the form${!outputname}
. This means examine the contents ofoutputname
and use what is there as the name of a variable whose value we require. We need to do this when handing the value returned byread_value
tocheck_value
, and when reporting that the value is invalid.
A test of this function might be as follows:
$ source read_value.sh
$ source check_value.sh
$ source read_and_check.sh
$ read_and_check "Enter slot: " slot "" '^(hpr[0-9]+)?$'
Enter slot: HPR42
Invalid input: HPR42
Enter slot:
$ echo "/$slot/"
//
- As before the functions are
source
d to ensure Bash knows about them. You only need to do this once for each one if testing them. If using these functions in a script you’d probably just copy all three into the script, which would achieve the same. - Then
read_and_check
is called to collect an HPR slot number (I use this in my episode preparation toolkit). The variableslot
is to hold the result, and there is no default. - The regular expression accepts lower-case ‘hpr’ followed by a number, or nothing at all.
- The first input is rejected because it uses upper-case letters, and the second null input is accepted with no problem.
- The echo shows that variable
slot
is empty.
Conclusion
These three functions are adequate for my use; I use them in a number of scripts I have written. They are not 100% bullet-proof. For example, if a regular expression is mistyped things could fail messily.
The business of passing information back from a function is messy, though it works. It can be streamlined by the use of nameref variables, which we will look at another time.
Please comment or email me with any improvements or changes you think would make these functions better.
Links
- Previous HPR episodes in this group Useful Bash functions:
- Download the read_value, check_value and read_and_check functions and the trace of read_value.
The command
local value="${1?Usage: check_value value list_of_regex}"
is used in this function. The parameter substitution expression
${parameter:?word}
means that the value of word is used as an error message and the script exits if the parameter is null or unset. Since we might want to use a parameter which is null we use the variant${parameter?word}
which merely checks for existence.↩Calling it concatenation is not strictly true. This is what is known as an AND list in Bash. Its generic form is:
command1 && command2
, and command2 is executed if and only if command1 returns an exit status of zero.↩