When your bash scripts start doing many things, then it may be a good idea to split them. Alternatively, you can implement subcommands to have multiple entry points into the script.

Usually this can be done very easily by few ifs. Take a look at this example:

This is very straightforward and is working pretty good:

$ ./script foo 1 2 3
subcommand foo 1 2 3
$ ./script bar 1 2 3
subcommand bar 1 2 3
$ ./script baz 1 2 3
subcommand baz 1 2 3

But it has one problem. This implementation of subcommands is basically a procedural logic. Even the simpliest kind of procedural logic can be often harder to understand than a fairly complex data structure. There is even a popular quote by Fred Brooks

Show me your code and conceal your data structures, and I shall continue to be mystified. Show me your data structures, and I won’t usually need your code; it’ll be obvious. – Fred Brooks

And there is also the Rule of Representation, one of the rules of the Unix Philosophy

Rule of Representation: Fold knowledge into data so program logic can be stupid and robust.

So how do we implement subcommands in a declarative way?

Declarative subcommands

Let’s jump straight into the working example.

Let us decode the magic line

Let’s start with ${1:-main}. This will return first argument passed to a script and if there is no argument it will return main. So if the script is invoked like this:

$ ./scripts foo

We get:

And if no argument is provided

$ ./scripts

It will become

Let’s go further. What if someone invokes the script with a subcommand that does not exist?

$ ./scripts one two three

Then we will get

The "${COMMANDS[one]} does not exist, so we will fall back to :-${COMMANDS[main]}, and we will finally get this:

So if someone calls the script with the wrong command, it means that he is really calling the main entry point. In our above case the main entry point is set to a function called usage() so in this case we are going to simply see the help message.

Arguments

The last nuance is handling the actual arguments passed to subcommands.

Let’s start with the main entry point. This is straightforward. The main entry point is called either with no arguments or all arguments passed to the script

$ ./script one two three
unknown command: one two three
echo Usage: script foo|bar|baz ARGUMENTS

But in the case of actual subcommands, they will also get all arguments, because if we are calling a script with subcommand like this:

$ ./script foo bar

We actually do this:

So our subcommand receives 2 arguments: foo and bar, but it should receive only bar. This is why we use the shift at the beginning of every subcommand

I hope it was not too scary.