Skip to content

Switch subcommand parsing to an enum Subcommand#666

Open
mkeeter wants to merge 5 commits into
masterfrom
mkeeter/subcommand-enum
Open

Switch subcommand parsing to an enum Subcommand#666
mkeeter wants to merge 5 commits into
masterfrom
mkeeter/subcommand-enum

Conversation

@mkeeter
Copy link
Copy Markdown
Contributor

@mkeeter mkeeter commented May 14, 2026

(staged on #661)

tl;dr

This PR adds an enum Subcommand containing every subcommand's arguments, so that all of our command parsing is done by the outer #[derive(Parser)].

How things stand

Subcommands are accumulated by the humility-bin's build.rs. In current code, every command crate has to define a fn init(), e.g.

pub fn init() -> Command {
    Command {
        app: UartConsoleArgs::command(),
        name: "console-proxy",
        run: console_proxy,
    }
}

The build script uses cargo-metadata to find all humility-cmd-* dependencies of humility-bin, then constructs a dcmds function which returns a table of subcommands:

fn dcmds() -> Vec<CommandDescription> {
    vec![
      CommandDescription {
            init: cmd_console_proxy::init,
            // every subcommand's codegen has the same docmsg ¯\_(ツ)_/¯
            docmsg: "For additional documentation, run \"humility doc {}\"."
       },
       // ...etc
    ]
}

Then, we dispatch based on command name; the first thing that every command's init function has to do is to parse its arguments from the cmd: Vec<String> trailing argument:

fn console_proxy(context: &mut ExecutionContext) -> Result<()> {
    let subargs = UartConsoleArgs::try_parse_from(&context.cli.cmd)?;
    // ...do stuff with subargs

Because these subcommands are dispatched by our code (and not known at #[derive(Parser)] time), we have to engage in additional shenanigans to get the command docstrings properly plumbed through to Clap.

Let's just not do that

After this PR, every subcommand is required to define a pub type Args (replacing the pub fn init()). This object must implement a new HumilitySubcommand trait, e.g.

pub type Args = UartConsoleArgs;
impl HumilitySubcommand for UartConsoleArgs {
    fn run(args: Args, context: &mut ExecutionContext) -> anyhow::Result<()> {
        console_proxy(args, context)
    }
}

We still use a build.rs to iterate over subcommands, but now we produce a pub enum Subcommand:

#[derive(::clap::Parser, Debug)]
pub enum Subcommand {
    Auxflash(cmd_auxflash::Args),
    ConsoleProxy(cmd_console_proxy::Args),
    Counters(cmd_counters::Args),
    Dashboard(cmd_dashboard::Args),
    Debugmailbox(cmd_debugmailbox::Args),
    Diagnose(cmd_diagnose::Args),
    Discover(cmd_discover::Args),
    Doc(cmd_doc::Args),
    Dump(cmd_dump::Args),
    Ereport(cmd_ereport::Args),
    Exec(cmd_exec::Args),
    Extract(cmd_extract::Args),
    // ...etc

This is embedded verbatim in the CLI argument object of humility-bin. With this organization, the clap parser Just Works™. We also generate a pub fn dispatch which takes the enum and calls the appropriate runner function with the Args variant as its first argument (removing the subargs parsing from the function itself).

Other changes

  • I removed humility_cmd::Command, which left Dumper as the only member of that crate, so I renamed it to humility-hexdump
  • I removed a few places where we call std::process::exit in favor of returning an ExitCode from main. This is still a little unorthodox, but I wanted to exactly preserve our existing CLI output. A few cases of exit remain, and I'll get to them later

@mkeeter mkeeter requested review from bcantrill, cbiffle and labbott May 14, 2026 19:21
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 8b3da22 to f3e41ab Compare May 14, 2026 19:45
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from a35dcb6 to 99df9fd Compare May 18, 2026 19:30
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from f3e41ab to 914a821 Compare May 18, 2026 19:30
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from 99df9fd to effa762 Compare May 18, 2026 19:53
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch 2 times, most recently from 8bc7cfb to 9b7da57 Compare May 18, 2026 20:21
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from 0b5c70a to 607b95e Compare May 18, 2026 20:54
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 9b7da57 to 1cefecd Compare May 18, 2026 20:54
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from 607b95e to b28c3a5 Compare May 20, 2026 19:40
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 1cefecd to 9474cbb Compare May 20, 2026 19:40
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from b28c3a5 to f98a018 Compare May 20, 2026 20:09
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch 2 times, most recently from 8c8ab59 to d462926 Compare May 20, 2026 20:45
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from f98a018 to 82dd66e Compare May 20, 2026 20:45
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from d462926 to 9029fba Compare May 20, 2026 23:03
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch 2 times, most recently from 1d7bdca to ced5065 Compare May 21, 2026 13:45
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 9029fba to 5aabda9 Compare May 21, 2026 13:45
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from ced5065 to ff30bea Compare May 21, 2026 14:09
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 5aabda9 to 282a801 Compare May 21, 2026 14:09
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from ff30bea to 38483a1 Compare May 21, 2026 14:24
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 282a801 to 7cf583b Compare May 21, 2026 14:24
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from 38483a1 to e6a5158 Compare May 21, 2026 14:37
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 7cf583b to 482e78f Compare May 21, 2026 14:37
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from e6a5158 to 70d7c6e Compare May 21, 2026 15:49
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 482e78f to 7c8ea72 Compare May 21, 2026 15:49
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 7c8ea72 to f92f5e5 Compare May 21, 2026 16:45
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch 2 times, most recently from 649d9d4 to c120380 Compare May 21, 2026 17:31
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from f92f5e5 to 01d62d8 Compare May 21, 2026 17:31
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from c120380 to f1b8bcc Compare May 21, 2026 17:52
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 01d62d8 to 4c980ca Compare May 21, 2026 17:52
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from f1b8bcc to e722703 Compare May 21, 2026 18:22
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 4c980ca to 4665ca1 Compare May 21, 2026 18:22
Copy link
Copy Markdown
Contributor

@labbott labbott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks okay but I want to look at the clap stuff a little closer.

We should also double check that humility-probless gets built properly and runs without libusb (just checking that we can get a help message in the switch zone)

@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 4665ca1 to 898cb40 Compare May 21, 2026 20:02
@mkeeter mkeeter force-pushed the mkeeter/hiffy-decode-flat branch from bfd295e to 24d7716 Compare May 21, 2026 20:20
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from 898cb40 to f49aa94 Compare May 21, 2026 20:31
Base automatically changed from mkeeter/hiffy-decode-flat to master May 21, 2026 20:50
@mkeeter mkeeter force-pushed the mkeeter/subcommand-enum branch from f49aa94 to 20d913b Compare May 21, 2026 20:50
@mkeeter mkeeter requested review from Copilot and jamesmunns May 21, 2026 22:59

This comment was marked as resolved.

Copy link
Copy Markdown

@jamesmunns jamesmunns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of naggy nits (feel free to mostly ignore), but overall looks good to me.

Also, I think Args might not be the best name we can pick, it's more like a subcommand? That being said, that name isn't great either since it's used in Clap.

The one note I do think is worth doing: I think we could reduce some boilerplate by just "inlining" the "run" functions into the trait impl.

Comment thread cmd/console-proxy/src/lib.rs Outdated
app: UartConsoleArgs::command(),
name: "console-proxy",
run: console_proxy,
pub type Args = UartConsoleArgs;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth just putting the pub type Args = ...; at the top of each crate, and maybe include a doc comment like "Used by humility-bin/build.rs to generate the top level CLI", so folks see it in the future, and realize it's not load-bearing in the crate itself, and just used for codegen duck-typing purposes.

Right now, we use a sort of inconsistent mix of Args, the specific type, and Self in the impls. It might make sense to just pick one.

Comment thread cmd/counters/src/lib.rs Outdated
Command { app: CountersArgs::command(), name: "counters", run: counters }
pub type Args = CountersArgs;
impl HumilitySubcommand for Args {
fn run(args: Self, context: &mut ExecutionContext) -> Result<()> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we don't even use the alias, we use Self. Can we do this everywhere?

Comment thread cmd/auxflash/src/lib.rs

fn auxflash(context: &mut ExecutionContext) -> Result<()> {
let subargs = AuxFlashArgs::try_parse_from(&context.cli.cmd)?;
fn auxflash(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we have this pattern of proxying the trait impl to a free function? It seems like a lot of syntactic noise for not a lot of benefit. Could this function just be the run method of HumilitySubcommand? Same comment for roughly all the other subcommands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but I was trying to minimize the diff here for ease of review. We could do a follow-up PR to mechanically inline everything?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you! I think if you put the trait impls where the free funcs used to be, it would diff pretty cleanly, but happy to defer that since it's just a nit anyway.

Comment thread humility-bin/build.rs
for cmd in cmds.iter() {
writeln!(
output,
" Subcommand::{}(args) => cmd_{}::Args::run(args, ctx),",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, here is the magic. Man I don't love this.

That being said: I can't really think of a better alternative here than linkme though, which would be a lateral level of shenanigans, and probably wouldn't work as well with clap. But whew.

Opened #678 to possibly rethink this, as I now understand why it has to be Like This.

Comment thread humility-cli/src/lib.rs Outdated
}

/// Trait representing a Humility subcommand
pub trait HumilitySubcommand: Parser {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit, do we ever not use these impls in a duck-typing kind of way?

I guess requiring a trait impl "by convention" helps us avoid forgetting to impl Parser for each of the Args types, but I'm not sure if that's much value since we're duck-typing the Args name in codegen anyway.

@mkeeter
Copy link
Copy Markdown
Contributor Author

mkeeter commented May 22, 2026

@jamesmunns Thanks for the feedback! I killed a few birds with one stone by introducing a new macro:

humility_cmd!(WritewordArgs, writeword);

This addresses a bunch of issues that you raised:

  • It's a loud marker that Something Special Is Happening Here (versus the duck-typed Args name)
  • It makes the "impl function calling free function" behavior less obviously weird
  • It hides the duck typing (and puts the standardized duck names in a single place)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants