Skip to content

A Docopt library suitable for simple CLI generation in microcontrollers

License

Notifications You must be signed in to change notification settings

andrewdodd/docopt-uc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Docopt uC

PyPI version

A Docopt library suitable for simple CLI generation in microcontrollers.

Why use this library? / Who might be interested?

This library leverages the approach of the docopt project to ease the pain in creating small to medium size command line interfaces for microcontrollers. That is...

you specify the output you want from --help, and this library creates the CLI for you!

Using this library you will be able to:

  • Specify your CLI in a docopt file format.
  • Generate a working CLI implementation to include verbatim in your project.
  • Receive a single header file that specifies all of the 'boilerplate' handler functions you have to implement.
  • Regenerate the implementation and header file at any time!
    • The header file is particularly nice, as when it is combined with a diff tool it shows clearly what has been added, removed or changed.

You might be interested in this if you are:

  • Creating a product that offers a UART CLI (i.e. does not have telnet / SSH or is not running linux).
  • Wanting to add a CLI to your Arduino or other bare-metal platform, but don't know where to start.
  • Sick of the tables of tables of pointers that your company currently uses for CLI structure.

How to use

This library should be installed however you normally install Python packages (i.e. it is usually recommended to use a virtual environment or something similar e.g. mkvirtualenv my-environment). To install from PyPI:

> pip install docopt-uc

To use with the built-in C and H file templates, simply provide the module name and your docopt file:

> docopt-uc mymodule mymodule.docopt

This will produce three files in the directory, the docopt_args.h file (of which you only need one in your project), and the two generated files. For example:

> ls
navalfate.docopt
> docopt-uc navalfate navalfate.docopt
> ls
docopt_args.h       navalfate.docopt    navalfate_autogen.c navalfate_autogen.h

The XXX_autogen.h file will specify a number of functions that need implementations provided (i.e. one for each command).

If you want to provide your own C and H file templates, that is also possible via options.

A more detailed example

This repository has an example folder that contains:

  • A slightly modified navalfate.docopt file
  • A basic main.c file, that sets up a more UART-like terminal environment (well...on my Mac it does) for the rest of the example to use
  • A basic cli_shell implementation, that implements basic command history, and does what you might expect a CLI in a small embedded project might do
  • The start of the implementation for the Naval Fate CLI functionality

There is an additional README in that folder which explains how to run the example and see this library in action.

I suggest you give it a go!

Why this library

There comes a time in every embedded-systems project where a CLI becomes a necessity. This could be for testing, for factory calibration, for use in the field, or just for letting a product manager play with CLI so they know you're doing something.

Almost all of these CLIs generate hours of discussion on "features" (both real product features and techical features) such as:

  • Should we have command completion, command history, etc etc?
  • How should we structure these menus best, both in code and actually at the terminal?
  • How can we allow the product manager the freedom to rearrange the CLI, but be confident we have the code right? and
  • Who is going to write all this boilerplate (because...let's face it...a lot of this code is easy and tedious but usually quite important)?

docopt-uc attempts to tackle some of them.

This library's position

This library takes a few hard positions on the items above, and comes to what I hope is a middle ground:

  1. Boilerplate is unavoidable

    CLIs are typically low-value, non-differentiating parts of your embedded products (unless you are Cisco...in which case they are part of an operator lock-in strategy). They are also usually really boring and repetitive to implement. However, they are also usually very important, as they are used for debugging, calibration, configuration, firmware upgrading, etc, and they often try to cut across the entire application (e.g. does the config update 'right now' and in the persistent store?). By flattening out the CLI menu structure; generating all the necessary function headers; and by handling the dispatch for you, using a tool like docopt-uc can give you confidence that nothing has been missed.

  2. Shell features are different to the CLI command handling

    This library believes command completion, command history, etc. all belong to the shell implementation (sure...command completion needs to know the commands, but that's a different story). This library is looking to solve the problem "what function should I call once the user pushes enter". It assumes the shell will do the work to parse the user's command into a standard (uint8_t argc, char **argv) signature, and it just needs to dispatch to the correct CLI handler function. The example demonstrates this distinction in that the shell implementation does not support quoted arguments (e.g. create ship "HMS Endeavour"), as this is a shell concern and I'm too lazy to add it (instead prefering just to lean on strtok()).

  3. Coping with change

    A CLI is often built incrementally in a project, to aid with development, testing, board-bring-up and well...anything. Product managers also evolve their understanding of products as they come to life. Too many times I have had to rearrange a CLI menu comprised of strange tables of tables of structs with unclear members such as {void *leaf, char *nodeName, void *nextLevel, void *userArg} etc, only to be unsure if every edge case was covered. This library believes the docopt file should be the product manager's responsibility. Using this library to generate the functions and dispatch stemming from the docopt file means that renames, restructurings, additions and deletions are all hopefully within the capabilities of any level of engineer.

  4. At the terminal

    This might be a bit controversial but...the implementation of the default template for this library doesn't worry about the order of the command words. While some people get a bit upset by this, I personally believe it is a better UX to be flexible. In the provided example, all of these commands would produce the same result:

    > ship create Titanic
    > create ship Titanic
    > create Titanic ship
    > Titanic create ship
    ... etc
    

    I agree a user doing this at your CLI is a bit strange (especially the last one), but the transposition error in the first two is pretty common in my experience and something that is easy to support if your technical design doesn't rest on tables of tables of tables (I really don't like the table thing, but I guess you figured that out).

Caveats

Obviously there are some caveats and limitations with this library. An inexhausive list includes:

There's a limit of 64 unique keywords (and an assumption of 64-bit integer support)

The design uses string comparison on each of the passed arguments to build an "opcode". It then uses this opcode to dispatch to the correct handler function (and consequently doesn't care about the order of those commands). I chose a 64-bit unsigned integer because 64-bit is probably supported by your compiler AND it is wide enough for you to hopefully never run out of unique command words.

If you would like to use a 32-bit or 16-bit or 128-bit wide opcode, you can provide your own docopt_args.h file (just be careful that the docopt-uc command does not over-write it...perhaps by passing the --no-docopt-args-h argument). The generated C file only checks that the number of keywords will fit in the opcode struct member.

You can try it out by setting the opcode size to uint8_t and running just make in the example, you should get a compile time error because there are 9 commands:

navalfate_autogen.c:65:1: error: 'assertion_failed___file___65' declared as an array with a negative size
CASSERT(LAST < (FIELD_SIZEOF(DocoptArgs, opcode) * 8), __file__);

If you do run out, you can always make two CLIs and stitch them together with your own dispatch function...something like:

static char const *handleCommand(uint8_t argc, char **argv) {
  if (strcmp("a-side", *argv[0]) == 0) {
    return Aside_processCommand(argc-1, argv++); // consume the first arg
  } else if (strcmp("b-side", *argv[0]) == 0) {
    return Bside_processCommand(argc-1, argv++); // consume the first arg
  }
  return "Not supported";
}

But you should probably reconsider both a) if this library is really appropriate, and b) if the extent of your CLI is really appropriate.

It is probably not that fast

Doing so many loops and string comparisons is possibly not that fast, but hey...it's a CLI, probably running over something with a rate specified in baud, how fast does it need to be?

It doesn't handle all docoptions

For example:

  • It doesn't handle the OR'd command options (e.g. mine (set|remove)), you have to do them the long way (i.e. mine set... mine remove...).
  • The default C implementation only supports named arguments with a -- prefix.

It does do a pretty good job otherwise, which includes:

  • Giving you the named arguments in two lists of namedLabel and namedValue, which have their preceeding -- and = removed respectively.
  • Giving you all positional arguments in the posValue array.

I think strikes a good balance at this stage, especially for embedded projects.

Thanks

To Kim Blomqvist's blog post for inspiring me to do this, and to both the docopt library and CS for making it possible.

Contributing

All contributions are welcome, also forking and evolving is too!