BASH: singular or plural done easy, a story of ternaries

The introduction

To avoid looking silly and being made fun the world over for having made a program that prints hair-raising butchered English like Copying 1 files, many programming languages have ternary constructs.

in PHP for instance one can use the ternary operator, ?:

<?php 
 $intFiles=1;

 printf("Copying %d file%s",$intFiles,1==$intFiles?null:'s');
?>

BASH is often insulted for having awkward and arcane syntax and as far as ternaries are concerned it doesn't disappoint. The closest equivalent is the case construct:

The first solution

file=1

echo -n "Copying ${file} "

case "${file}" in
 1) echo "file" ;;
 *) echo "files" ;;
esac

There are some problems with this approach. Not only do you have to split up your printing into several parts you also have to have a case construct for each test. What if you wanted to construct a sentence with several items being counted? Can you spell tiresome?

Luckly BASH, awkward arcane that it is, has a more florid solution, in the form of parameter expansion:

${parameter:-word} Use Default Values. If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.

In other words, using this expansion you can print the value of a variable, or, a default value if the variable isn't defined or set

A better solution

word=([1]=file)

number=1

echo "Copying ${word} ${word[${number}]:-files}"

OK, as long as you manage to keep track of the tongue-twisting mix of curly braces and brackets it is surprisingly easy and compact, at least when compared to the case construct.

Let's put it in a small script and see how it behaves with some edge cases:

#!/bin/bash
# save as /tmp/newcats.sh

catword=(
 [1]=cat
)


while `true`; do 

 read -p "How many? " cats || break

 echo "You have seen ${cats} ${catword[${cats}]:-cats}"

done
$ sh /tmp/newcats.sh 
How many? 0
You have seen 0 cats
How many? 1
You have seen 1 cat
How many? 2
You have seen 2 cats
How many? -1
You have seen -1 cat
How many? -2
You have seen -2 cats
How many? a
You have seen a cats
How many? @
/tmp/newcats.sh: line 13: @: syntax error: operand expected (error token is "@")
How many? 1.5
/tmp/newcats.sh: line 13: 1.5: syntax error: invalid arithmetic operator (error token is ".5")
How many? a few
/tmp/newcats.sh: line 13: a few: syntax error in expression (error token is "few")

Not bad, we see that negatives are handled correctly because for negative subscripts BASH substracts them from the total array count. So -1 means the highest item and since we have only one item, -1 is treated as 1, and -anything else is treated as plural. Great!

We do fall apart when it comes to floats and gibberish because indexed arrays need an integer subscript.

The obvious solution, therefore, is to use an associative array:

Solution the third

For associative arrays, BASH will not perform arithmetic on its subscripts so the handy trick with negatives will not work any more. This is however easily remedied by explicitly defining a -1 item.

BASH also requires explicit declaration of associative arrays through the -A flag.

#!/bin/bash
# save as /tmp/coolcats.sh

declare -A catword=(
  [1]=cat
 [-1]=cat
)


while `true`; do 

 read -p "How many? " cats || break

 echo "You have seen ${cats} ${catword[${cats}]:-cats}"

done
$ sh /tmp/coolcats.sh
How many? 0
You have seen 0 cats
How many? 1
You have seen 1 cat
How many? 2
You have seen 2 cats
How many? -1
You have seen -1 cat
How many? -2
You have seen -2 cats
How many? a
You have seen a cats
How many? @
You have seen @ cats
How many? 1.5
You have seen 1.5 cats
How many? a few
You have seen a few cats
How many? 
/tmp/coolcats.sh: line 14: catword: bad array subscript
You have seen  cats
How many? %#$%^#[]
You have seen %#$%^#[] cats
How many? '"`:{}
You have seen '"`:{} cats

Excellent! We are handling all cases correctly and the only thing we choke on is an empty input. That is easily helped though by using the same technique on the variable referencing the index.

Putting it all together

#!/bin/sh
# save as /tmp/ubercat.sh

declare -A catword=(
 [-1]=cat
  [1]=cat
)

declare -A is=(
 [-1]=is
  [1]=is
)

while `true`; do 

 read -p "How many? " cats || break

 echo "There ${is[${cats:-0}]:-are} ${cats:-0} ${catword[${cats:-0}]:-cats}"

done
~]$ sh /tmp/ubercat.sh 
How many? 
There are 0 cats
How many? 1
There is 1 cat
How many? 1.5
There are 1.5 cats
How many? -1
There is -1 cat
How many? -1.5
There are -1.5 cats
How many? a few
There are a few cats
How many? %#@%$^!
There are %#@%$^! cats

In conclusion

BASH is a versatile scripting language and although its syntax is awkward and possibly arcane, it can quite hold its own when it comes to manipulating strings. In this age of ever faster computers with more and more memory one is often seduced to outsourcing functionality in BASH scripts to utilities such as sed and awk and even full-size interpreters like perl and php. Digging into the BASH manual is daunting. The thing is a behemoth containing more than 100 pages of dry terse language. Spend some time experimenting however and you will discover some real gems and ultimately, doing things in the language is always going to be more efficient than doing it externally.

BASH pointer juggling

The introduction

BASH, like many programming and scripting languages, has pointer types. The word pointer says it all, it points to something else, it references something. You can use the contents of any scalar value in BASH as reference by using the ${!varname} syntax.

To create an actual pointer variable though you need to declare -n it.

Once you declare a variable as pointer you can use the ${!varname} syntax to find out where it points to

An example

#!/bin/bash
# save as /tmp/bashpointer.sh

targetVariable="the_target"

the_target="the value being pointed at"

echo "The contents of the ${targetVariable} variable is '${!targetVariable}'"

declare -n aRealPointer="the_target"

echo "The contents of the ${!aRealPointer} variable is '${aRealPointer}'"
$ sh /tmp/bashpointer.sh 
The contents of the the_target variable is 'the value being pointed at'
The contents of the the_target variable is 'the value being pointed at'

A task that I come across often is the processing of configuration files. We check if a variable is defined and copy it to the actual variable used throughout the script. This job is quite tedious and error-prone for anything more than a single variable.

This can be solved easily enough though with BASH pointers.

Let's make an array where each even-numbered index contains the variable to set, and each odd-numbered index contains the variable to read:

CHECKS=(
  "name" "profile_name"
 "value" "profile_value"
)

So we go through the list and check if the odd-numbered variable is set and if it is copy it to the even-numbered variable:

A first attempt

#!/bin/bash
# save as /tmp/refs.sh

profile_name="The name of the profile"
profile_value="The value of the profile"
name="___name"
value="___value"

CHECKS=(
  "name" "profile_name"
 "value" "profile_value"
)

set - ${CHECKS[@]}

typeset -n theref

while [ $# -gt 0 ]; do

 theref=$1

 [ -n "${!2}" ] && theref="$2"

 shift 2

done

echo " Name: ${name}"
echo "Value: ${value}"
$ sh /tmp/refs.sh
  Name: The value of the profile
 Value: ___value

That is unexpected! What is going on? The Value variable did not get set, and the Name variable has got the wrong value!

If we analyse this a bit more though, we can make the following observations:

  • The value of the Name variable changed therefore we have successfully created a pointer to the Name variable
  • The value meant for the Value variable has been put in the Name variable
  • The Value variable has not changed which means we have never been able to create a pointer to it

All of these observations spell out one thing: Once set, the pointer's target has not changed

This is corroborated by the BASH manual for declare -n:

declare [-aAfFgilnrtux] [-p] [name[=value] ...] typeset [-aAfFgilnrtux] [-p] [name[=value] ...] ... -n Give each name the nameref attribute, making it a name reference to another variable. That other variable is defined by the value of name. All references, assignments, and attribute modifications to name, except those using or changing the -n attribute itself, are performed on the variable referenced by name's value. The nameref attribute cannot be applied to array variables.

OK, according to this we can only update the value being pointed at by removing the -n attribute first:

Second attempt

#!/bin/bash
# save as /tmp/refs2.sh

profile_name="The name of the profile"
profile_value="The value of the profile"
name="___name"
value="___value"

CHECKS=(
  "name" "profile_name"
 "value" "profile_value"
)

set - ${CHECKS[@]}

while [ $# -gt 0 ]; do

 typeset -n theref="$1"

 [ -n "${!2}" ] && theref="$2"

 shift 2
 
 typeset +n theref

done

echo " Name: ${name}"
echo "Value: ${value}"
$ sh /tmp/refs2.sh 
 Name: profile_name
Value: profile_value

Success!!

In conclusion

There is no understating the merrits of pointers, yet the age old addage about great power and responsibility applies here too. Being responsible with BASH pointers means flipping the -n property prior to changing its target, or, using a local variable that goes out of scope between calls

.

BASH: singular or plural done easy, a story of ternaries

The introduction To avoid looking silly and being made fun the world over for having made a program that prints hair-raising butchered Eng...