When I first wrote toolbox, my goal was to remove boilerplate code from my scripts and improve code-reuse. Because my scripts became more robust and maintainable, I felt more comfortable writing long-running processes like daemons in bash, which led to the development of the ipc module for message-based communication. Because the module uses GPG for message signing, it can be tricky to set up, so I added the uipc module, which does mostly the same thing, but without message signing.
This put me in the situation where I had two modules in toolbox that did the same
thing and even had mostly the same functions, but that still could not be used
interchangeably because function names started with
ipc_ in the one module, and
uipc_ in the other, meaning that you could write your scripts to use one or
the other, but not both. In object-oriented programming, this could be easily
solved with inheritance or interfaces, both concepts that do not exist in Bash. So
I set out to write a small proof-of-concept that is now part of toolbox.
Declaring and implementing interfaces
A module can use the
interface() function to declare a set of functions that may
be provided (or overridden) by a module that is implementing the interface. Let’s
assume the following is the code of a module called greeting.
When the module is loaded, it declares an interface with the two methods
bye. The interface will be called
greeting, since that is the name of the
module that declares it. And the two methods of the interface will thus be called
What happens internally is that toolbox declares a
vtable (very simply said, a
table with function references) for the module, and creates the function stubs
greeting_bye(). When a function stub is called, it looks
at the vtable and calls whatever function is referenced there. By default, vtable
entries reference the function
method_not_implemented(), which lets the caller
know that they called an unimplemented method.
A module that wants to implement an interface can do so with the
function, which expects exactly one argument: the name of the interface. The
following snippet shows the module german, which implements
When the module german is initialized, toolbox checks if the module overrides any
of the interface’s methods. The
greeting interface has the two methods
bye, so toolbox checks if the functions
and updates the vtable accordingly.
To see why you would want something like this, let’s have a look at a script that uses these modules.
Note: The opt module used below is a command line parser.
opt_parse() function parses the command line according to the options that
were declared with
opt_add_arg(). The arguments to
opt_add_arg() have the
following meaning (in order):
- Short name of the option (e.g.
- Long name of the option (e.g.
- Flags (
v= option is followed by a value)
- Default value
- Description (for
Let’s assume this script is called
greet.sh and the modules have been placed in
a directory called
include next to it. When you now execute the script with
it will write the message
Hallo Peter to standard output. This is because the
value of the
--lang option defaults to
german, causing the module german to
be included (which causes greeting to be included).
Now if you wanted to add another language, all you have to do is add a new module. The following is the code of the module japanese.
To use the new module, none of the other files need to be modified. You can execute the script like
and it would greet you with the output
The example above declares a pure interface, which is an interface that does not implement any methods itself. Let’s assume we wanted to modify the example, so that it falls back to English if no language was specified.
The first thing we’d do is modify the greeting module so that it provides functions for English like the following.
What’s special about this case is that the module now has functions that collide
with the interface’s call stubs. When this happens, toolbox will first rename the
colliding functions, so the
greeting_hello() above becomes
___greeting_bye(). It will then generate the call
stubs and initialize the vtable so that the entry for
hello “points” to
___greeting_bye(). This way, the
interface-implements mechanism can be used to create modules that build on other
Since the greeting module now has a default behavior, we also need to change
greet.sh to account for that. This is as simple as changing the default language
greeting, by changing the first option declaration like this.
This is not the best solution though, since the help text that is printed when
--help on the command line will say that the
default value for
--lang is greeting. It would be nicer to include
unconditionally and load one of the other modules only if
--lang was passed on
the command line. There are many roads that lead to Rome.
Because the uipc module could now inherit code from the ipc module, I was able to remove about 80% of the former. What’s more, I could also modify foundry to support both IPC flavors with very little code changes.
Of course, this mechanism can’t compare with its counterparts in other languages, but it makes writing Bash scripts a lot more comfortable. I had concerns regarding the stability of this mechanism, but since I have been using it for half a year now and had literally zero problems with it, I merged it into stable and released toolbox 0.3.6 today.