Poor man's introspection in bash
August 19, 2020•424 words
Today I wanted to talk about bashunit, a unit testing library for bash scripts. This turned out to be a bigger topic, so you will have to wait a bit longer for the complete story. Instead I will talk about how to do poor man's introspection in bash, so that writing tests is less cumbersome.
In some sense, if you have to know this sort of obscure bash feature, it may be a good indication to stop, take a step back and run away. Still if you are reading this, chances are you run towards things like that, not away.
While writing bashunit, the library for unit testing bash scripts, I wanted to not have to enumerate test functions by hand. Doing that would be annoying, error-prone and just silly. Bash being a kitchen sink, must have a way to discover that instead. Most programming languages, with the notable exception of C, have some sort of reflection or introspection capability, where the program can discover something about itself through a specialized API. Details differ widely but the closer a language is to graphical user interfaces or serialization and wire protocols, the more likely it is to grow such capability. Introspection has to have a cost, as there must be additional meta-data that describes the various types, classes and functions. On the upside, much of this data is required by the garbage collector anyway, so you might as well use it.
Bash is very far away from that world. Bash is rather crude in terms of language design. Still it has enough for us to accomplish this task. The key idea is to use the declare
built-in, which is normally used to define variables with specific properties. When used with the -F
switch, it can also be used to list function declarations, omitting their body text.
We can couple that with a loop that reads subsequent function declarations, filter out the declaration syntax and end up with just the list of names. From there on all we need is a simple match expression and we can find anything matching a simple pattern. Et voilà, our main function, which discovers all the test functions and runs them.
#!/bin/bash
bashunit_main() {
local def
local name
declare -F | while IFS= read -r def; do
name="${def##declare -f }"
case "$name" in
test_*)
if ! "$name"; then
echo "bashunit: test $name failed"
return 1
fi
;;
esac
done
}
Tomorrow we will build on this idea, to create a very simple test runner and coverage analyzer.