Search code examples
rustclap

How can I prevent the last argument from needing to be quoted with clap?


I'm using clap and I get a unexpected behaviour when trying to parse arguments.

My command line tool is supposed to work like this

foo -u <user> <command>

e.g.:

foo -u jack echo s
foo -u paul ls -al

I need to get options such as user, but the <command> itself, I need to be the rest of the args.

The code below results in a behavior where I can't get the value of <command> unless it is quoted:

foo -u jack echo s
error: Found argument 's' which wasn't expected, or isn't valid in this context

Whereas this works fine:

foo -u jack 'echo s'

Is there any way of avoiding the quotes?

let matches = App::new("foo")
    .version("0.1")
    .arg(
        Arg::with_name("user")
            .short("u")
            .long("user")
            .required(true)
            .takes_value(true),
    )
    .arg(
        Arg::with_name("command")
            .help("The command to run")
            .required(true)
            .takes_value(true),
    )
    .get_matches();

I've also opened an issue on the clap repository.


Solution

  • By default, clap will only parse any argument once. This means that in -u jack echo s, it will parse -u jack as the "user" option, echo as the "command" argument, and have an argument s that it doesn't know what to do with (hence it "wasn't expected").

    To retrieve all trailing arguments you need to set .multiple(true) on the last argument (in your case "command") so it parses all the remaining arguments. Additionally set the following options on the clap command to avoid parsing remaining arguments as clap arguments:

    Here's an example:

    let matches = App::new("foo")
        .version("0.1")
        .setting(clap::AppSettings::TrailingVarArg)
        .setting(clap::AppSettings::AllowLeadingHyphen)
        .arg(
            Arg::with_name("user")
                .short("u")
                .long("user")
                .required(true)
                .takes_value(true),
        )
        .arg(
            Arg::with_name("command")
                .help("The command to run")
                .required(true)
                .takes_value(true)
                .multiple(true),
        )
        // parse as if program ran as:   foo -u paul ls -al
        .get_matches_from(&["foo", "-u", "paul", "ls", "-al"]);
    
    let command: Vec<&str> = matches.values_of("command").unwrap().collect();
    println!("{:?}", command); // ["ls", "-al"]
    

    Playground link