Bash notes
Here are a few things I learned, or re-learned, while reading through a neat solution to a Bash track exercise in Exercism.
I completed a very basic solution to the Proverb exercise in the Bash track on Exercism and proceeded to look at some of the solutions others had submitted. A beautifully simple and succinct solution from user Glenn Jackman had the most stars, and I wanted to share a few things I learned from it.
Here is the latest iteration of this solution:
#!/usr/bin/env bash
# There must be at least 2 positional parameters
# to enter the loop:
# `i` initialized to 1
# `i < $#` test passes only if 2 or more parameters
for (( i=1, j=2 ; i < $# ; i++, j++ )); do
echo "For want of a ${!i} the ${!j} was lost."
done
# And at least one parameter to print this:
[[ -n $1 ]] && echo "And all for the want of a $1." || :
Here are three things I learned or re-learned.
Incrementing multiple variables in a loop construct
The Loops and Branches chapter of the Advanced Bash Scripting Guide has examples of a C-style for loop that uses double parentheses. The last of these examples, and what we see here in the script, shows that we can actually initialise and increment more than one variable inside the double parentheses construct. In other words, in this line:
for (( i=1, j=2 ; i < $# ; i++, j++ )); do
we have both i
and j
being initialised and then incremented. This is a really neat solution for maintaining more than one indices. In case you're wondering, the $#
is the number of parameters passed to the script; each parameter is available via their position in variables like this: $1
, $2
, $3
and so on (they're referred to as positional parameters). So if the invocation is scriptname hello world
then $1
is hello
and $2
is world
.
Dynamic reference to positional parameters with indirect expansion
So while you can refer to e.g. the second parameter with $2
, what if you wanted to refer to the nth parameter, where n
was dynamic? This is the case in Glenn's solution; have a close look at this line:
echo "For want of a ${!i} the ${!j} was lost."
Here we want to refer to the "i-th" and the "j-th" parameter, whatever i
and j
are each time round the loop. Using simply a reference like this: $i
would resolve to the value of i
, which would be 1
for example (in the first iteration of the loop). But what we want is the value of the first parameter. This is why we see the !
which introduces a level of indirection. So here, we see ${!i}
and ${!j}
.
What happens is that these both resolve to "the value of the variable name in i
and j
". So in the first iteration of the loop, these then would resolve to the values of $1
and $2
. And in the second iteration, they'd resolve to the values of $2
and $3
.
Ensuring a successful script exit
The last line looks like this:
[[ -n $1 ]] && echo "And all for the want of a $1." || :
The || :
construct may look a little odd. But if one considers what :
does, it makes sense (indeed, it's explained by Glenn in the comments on this iteration of his solution). The :
is the "no operation" command, and I've covered it in a previous blog post - see The no-operation command : (colon). Essentially, it does nothing, successfully. Which means that if the [[ -n $1 ]]
condition is not true (i.e. $1
is empty) then the echo
will not execute, the script will then end anyway, but with a non-zero exit code, and this is not desired.
Using || :
here is like using || true
but perhaps more idiomatic to Bash.
Further reading
- The exercism and shell tags on this blog
- Exploring fff part 1 - main