PBS 147 of X: Arrays (Bash)
I had promised to cover both regular and associative arrays in this instalment, but due to some annoying open source politics, that didn’t work out. We’ll not be covering associative arrays in this series, because they can’t be done in a reliably cross-platform way at the moment (March 2023).
Matching Podcast Episode
Listen along to this instalment on episode 762 of the Chit Chat Across the Pond Podcast.
You can also Download the MP3
Read an unedited, auto-generated transcript: CCATP_2023_03_18
Episode Resources
- The instalment ZIP file — pbs147.zip
PBS 146 Challenge Solution
The challenge set at the end of the previous instalment was to:
[Write] a script that accepts a a whole number as an input, either as the first argument or from a user prompt, then prints out the standard n-times multiplication tables to the screen.
There was also the stipulation to use the bc
(basic calculator) terminal command to do the needed math. Finally, for optional extra credit, support could be added for a second argument to specify the maximum number the tables should go to.
You’ll find my sample solution in this instalment’s ZIP as pbs146-challenge-solution.sh
:
#!/usr/bin/env bash
# deal with the number to multiply
# start with the value of the first arg, if any
num="$1"
# make sure we have a valid value
until echo "$num" | egrep -q '^[1-9][0-9]*$'
do
read -p 'Enter a positive whole number: ' num
done
# figure out how high to count to
max=10
if [[ ! -z "$2" ]] # if there is a second arg, validate it
then
if echo "$2" | egrep -q '^[1-9][0-9]*$'
then
max=$2
else
echo 'invalid maximum value - must be a positive integer'
exit 1
fi
fi
# loop through the table
for n in $(seq $max)
do
# do the math
prod=$(echo "$n*$num" | bc)
# print the line
echo "$num x $n = $prod"
done
The majority of the code is unremarkable in that it just uses the basic programming primitives of variable assignments, conditionals, and loops. But, I do want to draw attention to a few subtleties.
Firstly, I’d never actually used the bc
command myself before starting my sample solution, so when I set the challenge I didn’t actually know how it worked either! I found the man
page needlessly complicated, but searching for some examples online I quickly realised that the simplest was to use bc
is to pipe an arithmetic expression to it as a string, e.g.”
echo '1+1' | bc
So, I used this approach with variable substitution to do my math:
prod=$(echo "$n*$num" | bc)
Secondly, I made liberal use of a convenient regular expression trick to check for whole numbers greater than zero without using mathematical operators — I checked for the pattern starts with one digit between 1 & 9, then zero or more digits between 0 & 9, and then ends, i.e. the PCRE regular expression ^[1-9][0-9]*$
.
Finally, I want to draw your attention to the fact that this script is not written to take input in the same way terminal commands usually do. The normal approach would be for the optional maximum value to be a named parameter, e.g. tables.sh 5 -m 11
. This thought serves as a little teaser for a very powerful bash utility we’ll be learning about soon specifically designed to parse a list of arguments that includes flags and named parameters, getopt
.
Arrays (Lists) in Bash
Note — you can play along with all the example snippets in a Bash shell, they are designed to work when entered in the order they appear in the show notes.
Like most languages, Bash support zero-indexed arrays. The concepts are the same as in JavaScript, and the syntax is similar in some regards, but also very different.
The most explicit way to create an empty array is with the declare
keyword and the -a
flag (for array):
declare -a desertList
The above command creates a new empty array named desertList
.
We can append values onto the ends of arrays with the +=
operator, e.g.:
desertList+=(waffles)
We can also write to a specific array index using the []
syntax you’re familiar with from JavaScript, e.g.:
desertList[1]=pancakes
To access specific elements of an array we use the []
syntax in conjunction with a braced version of the$
syntax:
echo "${desertList[0]}"
We can use the special array index @
to access the contents of the array as an argument list (space-separated list of strings):
echo "${desertList[@]}"
This is extremely useful, because it allows us to use arrays as lists of arguments for commands, including to the for
command:
for desert in ${desertList[@]}
do
echo "* $desert"
done
While creating an empty array and adding values to it with +=
is very common, you often know exactly what values you need when you are creating an array, so Bash lets you assign list of arguments directly to an array using the ()
syntax:
breakfastList=(porridge pop\ tarts 'French omelette')
Note that this is a list of arguments, so it is space-delimited, not ,
delimited like in many other languages. Also note that that means spaces in the values need to be escaped, or the values with spaces need to be quoted. So, the above example creates an array with three items, 'porridge'
, 'pop tarts'
, and 'French omelette'
To prove this, let’s look at the syntax for calculating the length or an array. This syntax is extremely obtuse, but it’s an important one to learn. Anyway, to get the length of the array you need to combing the #
(count
) operator with the $
, []
and @
syntaxes we’ve already seen:
echo "there are ${#breakfastList[@]} items in the breakfast list"
Note that we can use the #
operator to count the length of specific items (in characters) within the array, or indeed, in any variable:
# length of string in array
echo "There are ${#breakfastList[0]} letters in ${breakfastList[0]}"
# length of string in variable
cat=Felix
echo "There are ${#cat} letters in $cat"
Finally, you can use the @
syntax to include all the elements from an existing array into a new array, e.g.:
brunchList=("${breakfastList[@]}" burger fries)
Note that if you don’t quote the inner array then the spaces within the strings in that array will be treated as separators, so in our example 'French omelette'
would become two entries in the new array, 'French'
and 'omelette'
.
A Word on Associative Arrays
Associative arrays were added in version of 4 of Bash in 2009, and did not exist in previous version of Bash. Associative arrays also exist in Zsh, but the syntax is different, so Bash 4 code that uses some parts of the associative array syntax won’t work in Zsh.
Bash 4 changed more than just the shell’s features, it also changed the shell’s license from GPLv2 to GPLv3. Version 3 of the GPL is controversial, and many commercial companies have decided to avoid the license. This is why even the very latest version of macOS (as of March 2023) still ships with Bash version 3, and why Apple have started to default their interactive terminals to Zsh.
Most Linux distros ship with Bash 4 and Zsh, so switching the shebang line to Zsh (#!/usr/bin/env zsh
) and using the Zsh syntax for associative arrays seems like a good option, but alas, the Windows Subsystem for Linux does not ship with Zsh. It is of course possible to install Zsh on Windows, and Bash 4 on the Mac (e.g. via HomeBrew, but there’s no way to write a truly portable shell script that uses associative arrays at the moment.
An Optional Challenge
Write a script to take the user’s breakfast order.
The script should store the menu items in an array, then use a select
loop to to present the user with the menu, plus an extra option to indicate they’re done ordering. Each time the user selects an item, append it to an array representing their order. When the user is done adding items, print their order.
For bonus credit, update your script to load the menu into an array from a text file containing one menu item per line, ignoring empty lines and lines starting with a #
symbol.
Final Thoughts
At this stage we’re nearing the end of our Shell Scripting journey, all that remains now is to learn how to make our scripts behave like regular terminal commands by supporting named arguments and flags, and supporting what I call ‘terminal plumbing’.
One More Thing - Git for Keyboard Maestro
At the very end of this instalment, Allison mentioned a post she did about using Git to manage version control for Keyboard Maestro. The article can be found at www.podfeet.com/…