[clap]: fixed array

Hi,
When using clap, I would like to have an argument that is of a fixed array size:

e.g

struct Args {
    #[arg(
        long = "stages",
        help = "Comma-separated list of stages to run",
        default_value = ["clear","configure","build","install"],
    )]
    stages: [&str; 4]
}

However, I keep getting the following error and am not quite sure why this won't work. Any help is appreciated.

error[E0277]: the trait bound `clap::builder::OsStr: From<[&str; 4]>` is not satisfied
    --> src/cli_args.rs:21:25
     |
21   |         default_value = ["clear","configure","build","install"],
     |         -------------   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<[&str; 4]>` is not implemented for `clap::builder::OsStr`
     |         |
     |         required by a bound introduced by this call

If you want it to be comma-separated like that, you'll have to write your own ValueParser function. It would look something like this (Rust Playground):

use clap::{builder::ValueParser, Parser};

fn parse_stages(arg: &str) -> Result<[String; 4], &'static str> {
    arg.split(',')
        .map(str::to_string)
        .collect::<Vec<_>>()
        .try_into()
        .map_err(|_| "must specify 4 stages")
}

#[derive(Parser)]
struct Args {
    #[arg(
        long = "stages",
        help = "Comma-separated list of stages to run",
        default_value = "clear,configure,build,install",
        value_parser = ValueParser::new(parse_stages),
    )]
    stages: [String; 4],
}

fn main() {
    let args = Args::parse_from(["example", "--stages=a,b,c,d"]);
    println!("{:?}", args.stages);
    let args = Args::parse_from(["example"]);
    println!("{:?}", args.stages);
    let args = Args::parse_from(["example", "--stages=a,b,c,d,e"]);
    println!("{:?}", args.stages);
}
1 Like

@LegionMammal978 Thank you. That worked.

Naive question 1: how can I change parse_stages to accept If the length of stages is not exactly 4?
for example, the stages could only have 1 entry ( e.g --stages=clear ) and that should be still valid.

Naive question 2: how can I change this so the "clear","configure","build","install" are only the valid keyword entries that stages could have?

This is a bit tricky, since if parse_stages returns a Vec<String>, then Clap will incorrectly assume that you're trying to get multiple arguments. So we have to trick Clap a bit by returning a Box<[String]> instead. If you don't plan on adding or removing any elements, then using it shouldn't be any different from a Vec. Rust Playground:

use clap::{builder::ValueParser, Parser};

fn parse_stages(arg: &str) -> Result<Box<[String]>, String> {
    arg.split(',')
        .map(|stage| {
            if ["clear", "configure", "build", "install"].contains(&stage) {
                Ok(stage.to_string())
            } else {
                Err(format!("unknown stage '{stage}'"))
            }
        })
        .collect()
}

#[derive(Parser)]
struct Args {
    #[arg(
        long = "stages",
        help = "Comma-separated list of stages to run",
        value_parser = ValueParser::new(parse_stages),
        default_value = "clear,configure,build,install",
    )]
    stages: Box<[String]>,
}

fn main() {
    let args = Args::parse_from(["example", "--stages=clear,configure"]);
    println!("{:?}", args.stages);
    let args = Args::parse_from(["example"]);
    println!("{:?}", args.stages);
    let args = Args::parse_from(["example", "--stages=clear,test"]);
    println!("{:?}", args.stages);
}

(Also, note that this won't accept an empty list of stages, since it will interpret it as a single empty string. To change that, you could just check for an empty string before splitting.)

1 Like

That's not a String then, but a custom enum with those 4 variants.

Indeed, it's not too much extra work to parse the stage names into an enum (Rust Playground):

#[derive(Copy, Clone, Debug)]
enum Stage {
    Clear,
    Configure,
    Build,
    Install,
}

fn parse_stages(arg: &str) -> Result<Box<[Stage]>, String> {
    arg.split(',')
        .map(|stage| match stage {
            "clear" => Ok(Stage::Clear),
            "configure" => Ok(Stage::Configure),
            "build" => Ok(Stage::Build),
            "install" => Ok(Stage::Install),
            _ => Err(format!("unknown stage '{stage}'")),
        })
        .collect()
}

#[derive(Parser)]
struct Args {
    #[arg(
        long = "stages",
        help = "Comma-separated list of stages to run",
        value_parser = ValueParser::new(parse_stages),
        default_value = "clear,configure,build,install",
    )]
    stages: Box<[Stage]>,
}

Appreciate it. This was helpful.

@home3d2001 I believe this can be simplified further:

  • you can derive ValueEnum, and
  • there's no need for an explicit ValueParser.
fn parse_stages(input: &str) -> Result<Box<[Stage]>, String> {
    input
        .split(',')
        .map(|s| Stage::from_str(s, true))
        .collect()
}

#[derive(Clone, Debug, Parser)]
struct Args {
    #[clap(short = 's', long = "stage", default_value = "build", value_parser = parse_stages)]
    stage: Box<[Stage]>,
}

Playground

2 Likes

@H2CO3 That is really nice. I like it!

Thank you.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.