Some time ago I posted about a declarative approach to the implementation of subcommand parsing in Bash scripts. Turns out, that this approach can be extended to also support long (like --long) and short (like -s) option parsing.

Main function and multiple entry points

In my previous post about parsing subcommands, the idea for declarative subcommands was this:

This way, if we invoke ./script foo X Y Z, it will invoke foo() function and the arguments are going to be X Y Z (the same goes for bar subcommand). If we invoke ./script X Y Z it will invoke main() function (default entry point) and the arguments going to be X Y Z. This approach to subcommand parsing can be also utilized for option parsing.

Option parsing

We can implement option parsing by extending the approach to argument parsing in a few steps.

Step one:

Wrap the argument parsing one-liner into a function (let’s call it argparse)

We are doing this, because we will be invoking the “argument parsing one-liner” multiple times, so that we can parse multiple options.

Step two

Add script options to COMMANDS associative array.

As an example we will add two options: - --long ARG - long option, which accepts a single mandatory argument - -s ARG - show option, which accepts a single mandatory argument

Here is the COMMANDS array after the changes:

So what is set_value? It is a name of the function, which does two things:

  1. Parses an option
  2. Runs argparse again

This function can be implemented like this:

Function set_value() will parse an option, and run argparse() again, which will parse next option or call ${COMMANDS[main]} if there are no more options to parse or if an option does not exist.

Then in the main() function (which is connected to “main” entry point) we can access script options using $OPT_{option_name} variables.

$ ./script --long hello -s world
hello
world

Support for “flag” options

Some options can act as simple on/off switches. We can implement those using set_true function, which can be implemented as a simple wrapper for set_value.

Then we can pass --switch option without arguments and it will be set to true

$ ./script --switch
true

All of this works great, but has one potential problem. Currently our option parsing works only for the “main entry point”. If we wanted to pass these options to specific subcommands it wouldn’t work.

Here is an example that will not work:

$ ./script foo --long hello

In the above example we will invoke foo entry point and stop argument parsing. However if we specify --long option before the foo, then it will be parsed and the foo() function will be executed with $OPT_long set to hello.

./script --long hello foo

But is this really a problem? I think this makes perfect sense. The --long option is a “top level” option for the script, and it affects the entire program.

But nevertheless, what if we wanted to implement option parsing for a specific subcommand? In other words, what if we wanted to make this work:

$ ./script foo --long hello

Turns out, there is a way.

Different set of options for subcommands

We will add option parsing to one of our entry points. Let’s take foo entry point as an example. The first thing we need to do is to wrap foo() in a function, which we will call foo_parser()

And instead of calling foo() when foo subcommand is selected we will call foo_parser()

Now in the foo_parser(), let’s not call foo() directly. Instead overwrite COMMANDS array and call foo() through argparse(). This allows us to handle options specific for foo entry point.

After the changes, this is supported:

$ ./script foo --long hello

Just one little detail - since foo() is called as a main entry point in foo_parser(), then we no longer need to shift its name from the arguments.

So originally, the foo entry point used a shift to get rid of its name from arguments

But now, it is not needed.

Summary

Usually option and argument parsing is implemented using while loop combined with case statement. Using an associative array and an “argument parsing one-liner”, we can implement argument and option parsing in a declarative way.