5

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Marcelo Boeira
  • 860
  • 8
  • 22
  • 3
    Please take this opportunity to commit to a choice between `system(3)` semantics that expect a single argument with a shell command (like `su`, `ssh` and `parallel`) and `execve(2)` semantics that expect multiple separate arguments (like `sudo`, `xargs`, `find`). Don't try to accept both versions, it's only fragile and confusing. – that other guy Mar 25 '19 at 20:24
  • @thatotherguy I'll read about it! Thanks for the info! I was aware of such patterns but unaware they had name/specifications. Awesome! – Marcelo Boeira Mar 25 '19 at 20:30

2 Answers2

9

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

njam
  • 1,233
  • 14
  • 16
Frxstrem
  • 38,761
  • 9
  • 79
  • 119
  • Works partially, so if you try something with flags doesn't work, unfortunately, e.g.: `foo -u jack ls -al` fails because of the `-al` at the end. – Marcelo Boeira Mar 25 '19 at 21:08
  • @MarceloBoeira To distinguish arguments from options, you can use a double dash `--`, after which none of the arguments will be interpreted as options: `foo -u jack -- ls -al`. It might also be able to tell clap to not parse options after the first non-option argument, but I'm not certain about that. – Frxstrem Mar 25 '19 at 21:21
  • I'm actually considering dropping clap, because it seems easier to do it without it, do something like `foo user command` – Marcelo Boeira Mar 25 '19 at 21:24
  • 4
    @MarceloBoeira Looking over the documentation, it looks like [`.setting(AppSettings::TrailingVarArg)`](https://docs.rs/clap/2.32.0/clap/enum.AppSettings.html#variant.TrailingVarArg) does exactly that. – Frxstrem Mar 25 '19 at 21:24
  • It's not that simple, the Trailing VarArg allows that, but you still have to do some parsing yourself... – Marcelo Boeira Mar 27 '19 at 09:01
  • Using both [`AppSettings::TrailingVarArg`](https://docs.rs/clap/2.33.1/clap/enum.AppSettings.html) and [`AllowLeadingHyphen`](https://docs.rs/clap/2.33.1/clap/enum.AppSettings.html#variant.AllowLeadingHyphen) it's possible to parse remaining args like `-al` without using `--`. Updated the answer. – njam Jul 03 '20 at 15:21
2

As of the current clap version (3.2.12), the above code is no longer valid since the API has gone through some changes plus deprecations. For e.g.

  1. short now accepts a char instead of a &str
  2. with_name is now deprecated and replaced with new
  3. setting is now deprecated in favor of specific function names for each configuration
  4. multiple now replaced by multiple_values
  5. values_of deprecated in favor of get_many

The updated snippet is:

#[test]
fn test_parse_config() {
    let matches = App::new("foo")
        .version("0.1")
        .allow_hyphen_values(true)
        .trailing_var_arg(true)
        .arg(
            Arg::new("user")
                .short('u')
                .long("user")
                .required(true)
                .takes_value(true),
        )
        .arg(
            Arg::new("command")
                .help("The command to run")
                .required(true)
                .takes_value(true)
                .multiple_values(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.get_many::<String>("command").unwrap().
        map(|s| s.as_str()).collect();
    println!("{:?}", command); // ["ls", "-al"]
}
Sanjay T. Sharma
  • 22,857
  • 4
  • 59
  • 71