rustclap

rust clap default_values_t for a vector, where flag may repeat and may not


    #[clap(long = "type", num_args = 0.., default_values_t = [MyType::a], value_parser = MyType::from_string)]
    pub name_types: Vec<MyType>,

    #[clap(long = "name", num_args = 1..)]
    pub names: Vec<String> 

I have a similar code (MyType is a enum with two possible values, assume "a" and "b"). I can call my cli: my-cli --name andrew --type a --name billy --type b and it will create names as ["andrew","billy"] and name_types as [MyType::a, MyType::b]. But I wanna call it as my-cli --name andrew --name billy --type b and expect it to work as well (default value for type, if it's not preset to be MyType::a). Also, i want it to keep the order, i.e. if i call cli as my-cli --name andrew --name billy --type b --name carol => name_types = [a,b,a]


Solution

  • From what I can tell, you are implicitly expecting a 1-to-1 relation between the elements of names and of name_types. Well, you haven't told clap about it, nor is clap suitable for specifying this kind of non-trivial logical relations - that's not the job of a command line parser.

    Instead, probably the best way to do this is to keep your clap stuff simple. Then write custom logic to "post-process" clap's output into a structure that properly encodes the logical relation, and use that in your core application.

    Here's what I would write. Bear in mind I personally prefer a declarative and functional style, but this could be just as well written imperatively.

    use clap::{CommandFactory, FromArgMatches, Parser, ValueEnum};
    
    // ValueEnum is probably the better way to do this
    // unless your type is actually more complicated than in this example
    #[derive(Debug, Copy, Clone, ValueEnum)]
    enum MyType {
        A,
        B,
    }
    
    #[derive(Debug, Parser)]
    struct Cli {
        // #[clap(...)] is deprecated
        #[arg(long = "type")]
        pub name_types: Vec<MyType>,
    
        // num_args doesn't do what you probably think it does
        // see https://github.com/clap-rs/clap/issues/4507#issuecomment-1372250231
        #[arg(long = "name", required = true)]
        pub names: Vec<String>,
    }
    
    // this type properly expresses the logical relation
    // "each name must have an associated type"
    #[derive(Debug)]
    struct User {
        name: String,
        name_type: MyType,
    }
    
    fn main() {
        let matches = Cli::command().get_matches();
        let Cli { name_types, names } = Cli::from_arg_matches(&matches).unwrap();
    
        // get the applicable index range of the `--type` option for each name
        // the `--type` options found between a `--name` and the next is applicable
        let applicable_type_index_ranges = {
            // get indices of `names`
            let mut name_indices = matches
                .indices_of("names")
                .into_iter()
                .flatten()
                .collect::<Vec<_>>();
            // for the last `--name` option, the upper index is unbounded
            name_indices.push(std::usize::MAX);
            name_indices
                .windows(2)
                .map(|window| match window {
                    [start, end] => (*start, *end),
                    _ => unreachable!(),
                })
                .collect::<Vec<_>>()
        };
        assert_eq!(names.len(), applicable_type_index_ranges.len());
    
        // pair the `--type` options with their indices
        let name_types_with_indices = {
            let type_indices = matches
                .indices_of("name_types")
                .into_iter()
                .flatten()
                .collect::<Vec<_>>();
            assert_eq!(name_types.len(), type_indices.len());
            name_types.into_iter().zip(type_indices).collect::<Vec<_>>()
        };
    
        // construct `User`s with types properly assigned
        let users = names
            .into_iter()
            .zip(applicable_type_index_ranges)
            .map(|(name, (start_idx, end_idx))| {
                let name_type = name_types_with_indices
                    .iter()
                    // only `--type` options within the range are applicable
                    .filter_map(|&(t, idx)| (idx > start_idx && idx < end_idx).then_some(t))
                    // assuming you want the last applicable `--type` to take precedence
                    .last()
                    // your default value if no `--type` is present
                    .unwrap_or(MyType::A);
                User { name, name_type }
            })
            .collect::<Vec<_>>();
    
        dbg!(users);
    }
    
    $ cargo run -- --name romeo --type b --name juliett
    [src/main.rs:86:5] users = [
        User {
            name: "romeo",
            name_type: B,
        },
        User {
            name: "juliett",
            name_type: A,
        },
    ]
    

    As you can see, it can be done but it's non-trivial, with plenty of places where it would be weird for clap to make an opinionated design decision for you.