funbiscuit / embedded-cli-rs

CLI in Rust with autocompletion, subcommands, options, help and history for embedded systems (like Arduino or STM32)
Apache License 2.0
79 stars 4 forks source link

derive `Command` for structs? #15

Open MabezDev opened 2 months ago

MabezDev commented 2 months ago

I have some settings I wish to be able to configure over a CLI, however, the setting enum often has structs which have most of the configuration information.

pub struct NetworkSettings {
    dhcp: bool,
}

pub struct UartSettings {
    baud: u32
}

pub enum Setting {
    /// Global network settings
    Network(NetworkSettings),
    /// Settings for a given uart, the first param is the uart number
    Uart(usize, UartSettings),
}

As things currently stand, the derive can't handle this, and would require me to duplicate the fields in the enum.

I'm wondering:

I'm happy to contribute, I just need a bit of direction as proc_macros are my weakest area of Rust.

funbiscuit commented 2 months ago

Currently structs are only supported as subcommands. You can find example in desktop example:

#[derive(Debug, Command)]
enum BaseCommand<'a> {
    /// Control LEDs
    Led {
        /// LED id
        #[arg(long)]
        id: u8,

        #[command(subcommand)]
        command: LedCommand,
    },

    /// Control ADC
    Adc {
        /// ADC id
        #[arg(long)]
        id: u8,

        #[command(subcommand)]
        command: AdcCommand<'a>,
    },

    /// Show some status
    Status,

    /// Stop CLI and exit
    Exit,
}

And subcommands can be used as single field of tuple variant:

#[derive(Debug, Command)]
enum BaseCommand<'a> {
    /// Control ADC
    Adc(
        #[command(subcommand)]
        AdcCommand<'a>
    ),
}

Flattening can be supported but it's a bit tricky. When generating parser implementation you will need to try pass current argument to parser implementation to struct and if it fails, try to parse other fields of current variant. And there might be multiple fields that should be flattened.

Contributions are welcome, but this might be a hard one. If you want to dig a little bit, try to start from analyzing actual generated code of examples (use cargo expand). And then figure out how to modify parser implementation. After that it will be easy to implement that implementation in macro.

To be more exact, how do you think generated code for autocomple, parser and help should look like for following example?

pub struct NetworkSettings {
    dhcp: bool,
}

pub struct UartSettings {
    baud: u32
}

pub enum Setting {
        /// Global network settings
    Network(
             #[command(flatten)]
             NetworkSettings
         ),
         /// Settings for a given uart, the first param is the uart number
    Uart{
            id: usize,
             #[command(flatten)]
            uart: UartSettings
         },
}
MabezDev commented 1 month ago

Hmm, this is indeed a bit tricky. I've been playing around with it locally, and made some progress (or rather gained some familiarity with how the derives actually work).

When generating parser implementation you will need to try pass current argument to parser implementation to struct and if it fails

I'm planning on avoiding this by treating flattened in order of placement in the enum variant, so I guess a bit like positional. Let me know if you think this is the wrong path.

I'm starting to think that Command is the wrong thing to base this off. My initial attempts tried to shoe horn the scaffolding for SubCommand into a flatten, but because it's a command, and commands only are parsed from RawCommand it must have a name. In reality we want to treat the struct as inlined arguments.

My idea now is to introduce an Args derive which works quite a lot like RawCommand parsing, but drops the requirement of a name. I'll see where I get with this.

funbiscuit commented 1 month ago

I'm planning on avoiding this by treating flattened in order of placement in the enum variant, so I guess a bit like positional.

The moment you have flattened args, someone will combine them with other args. And forcing specific order for user will be very misleading. Suppose you have following command:

pub struct UartSettings {
    #[arg(long)]
    baud: u32
}

pub enum Setting {
    Uart{
            #[arg(long)]
            id: usize,
            #[command(flatten)]
            uart: UartSettings
         },
}

By forcing order your cli will fail to parse this command:

uart --baud 12 --id 0

and will only work with this command:

uart --it 0 --baud 12

So having support for arbitrary order when using flattened structs is crucial.

I'm starting to think that Command is the wrong thing to base this off.

You're probably right. Initially I tried to mimic clap approach but it didn't fit well in all places. Mostly because of embbedded stuff and living without dynamic memory. If you figure out better API for command description with macros - that would be great. That would be a breaking change of course but it's worth it.

I can also highlight that another distinction between commands and args is that commands are modeled as enums and args - as structs. This seems convenient but maybe not a right choice.

MabezDev commented 1 month ago

The moment you have flattened args, someone will combine them with other args. And forcing specific order for user will be very misleading. Suppose you have following command:

Hmm, but are we in agreement at least that for any flattened field the arguments should be passed in any order, but grouped. In other words:

pub struct UartSettings {
    #[arg(long)]
    baud: u32,
    #[arg(long)]
    other: u32
}

pub struct UartSettings2 {
    #[arg(long)]
    something: u32
     #[arg(long)]
    something_else: u32
}

pub enum Setting {
    Uart{
            #[arg(long)]
            id: usize,
            #[command(flatten)]
            uart: UartSettings,
            #[command(flatten)]
            uart: UartSettings2
         },
}

A user should enter --baud and --other together (but the order doesn't matter) and should not interleave args for UartSettings2.

I think without this restriction it becomes very hard to implement, at least with the way I had in mind.