Integrating clap and rustyline
My latest project is a command line interpreter written in Rust. As such, I wanted to use rustyline for the line reader and clap to parse that line. This post explores how I integrated these two libraries to leverage a single source of data for both command parsing and tab completions.
TLDR; here is a POC gist
Why I chose to do this
One behavior of Rust is informing the compiler of data that is related, so I thought it made sense to have my tab completion be based off of clap’s argument parsing. In this way, whenever I add, remove, or modify the argument parsing code, the tab completion will stay in sync.
Premise
Once I explored both libraries, I came up with an idea: I can define
my clap App
using YAML and parse the same YAML to feed into the tab
completion. This approach has a few drawbacks (no validator support),
however I believe the convenience is worth it.
In order for this to be viable, however, the YAML has to be part of
the compiled binary. We cannot do this parsing at runtime, because we
don’t want to allow the user to modify it. Clap already has a load_yaml!
macro that does this, which uses the include_str!
macro underneath to include
the file at compile time. Perfect.
Actually doing it
This was a lot more challenging than I make it sound, and it took me a couple of days to get it working. I’ll explain the main logic here, but if you are looking for a POC to copy and paste, here is the gist.
Summary
- Parse the YAML into a recursive structure (each struct has a list of completions)
- Split the input string into words
- Walk the structure using the input tokens
- Filter candidates by the start of the last word
Example: “set environment l”
1. LUT = {"create": ["request", "variable"], "set": [{"request": [], "environment": ["local", "stage"]}]}
2. "set environment l" = ["set", "environment", "l"]
3. LUT["set"]["environment"] = ["local", "stage"]
4. Filter by "l" = ["local"]
Now this sounds simple, but almost all of the complexity is in step 1: Creating the structure from the clap YAML file. My goal was to have the completer work for both subcommands and aliases, but only complete the subcommand name. A quick example should clarify what I mean:
subcommands:
- create:
visible_aliases: ["new", "add", "c"]
subcommands:
- request:
visible_aliases: ["req", "r"]
- variable:
visible_aliases: ["var", "v"]
"c" should complete to "create "
"create r" should complete to "create request "
"new r" should complete to "new request "
"n" should not complete to anything
At the time of writing, this is how I designed this structure. Describing it, however, makes me think I should treat the subcommand name and visible aliases the same, and ignore non-visible aliases. Ah well, time will tell.
YAML Parsing
There’s not much to go into here. Once you have your working clap
YAML file, you can parse it with serde_yaml
and recursively build
out the relevant information from the tree.
Setting up rustyline
I have to admit, setting up rustyline with custom completions is a pain. I believe there is work being done to make it easier, but the docs need improvement. Hopefully my POC can help others simplify the process a bit.
For rustyline, we need to create a helper type that implements
Helper
, Hinter
, Highlighter
, Validator
, and Completer
.
Luckily, there are no mandatory functions for each trait, so most
of those are just impl <Trait> for <Struct> {}
. We will, however,
provide an implementation for Completer
. We will make it get the recursive
struct we generated, then walk the tree to find the list of possible
completions. The last step is to filter out the candidates by the last word
being typed, so cr<TAB>
will actually complete instead of giving you a list
of options.
Conclusion
There are plenty of ways to extend this to be even better by parsing
more of the details of the clap YAML file (e.g. possible_values
,
flag names, etc.), however this provides a very good start that
covers 90% of my project’s common commands.