2

I'm looking for a way to initialize a structopt Vec field with multiple items by default. I can do it for a single item with:

use structopt::StructOpt;

#[derive(Debug, StructOpt)]
pub struct Cli {
    #[structopt(default_value = "foo")]
    foo: Vec<String>,
}

fn main() {
    let cli = Cli::from_iter(Vec::<String>::new());
    assert_eq!(cli.foo, vec!["foo"]);
}

But how to make cli.foo to be equal let's say vec!["foo", "bar"] by default?

imbolc
  • 1,620
  • 1
  • 19
  • 32
  • You can do it the rusty way, by explicitly implementing `Default` for a wrapper type of the member, see the [docs](https://docs.rs/structopt/0.3.17/structopt/#default-values). From what I see, you'll need to implement Display and friends (FromStr, ToString, ...) yourself. – L. Riemer Aug 26 '20 at 07:25
  • @L.Riemer thanks, just `FromStr` seems to be enough (posted as an answer) – imbolc Aug 26 '20 at 08:54
  • That's a little different from what I had in mind, should work too though. – L. Riemer Aug 26 '20 at 09:15
  • @L.Riemer Would you post the solution when you have a minute? I'm new to rust so I probably missing something. – imbolc Aug 26 '20 at 12:49
  • There is absolutely no need for that. Your solution is just fine, and it is well defined. Implementing `FromStr` tells the compiler how to translate the `"foo,bar"` in `(default_value = "foo,bar", long)` to the wrapper type (and how to parse from the cli). I would instead have written `(default_value, long)` and implemented `Default` for the wrapper type, but that's a matter of taste. You may accept your answer, btw. – L. Riemer Aug 26 '20 at 18:37
  • Thanks, I will :) The interface isn't perfect though: `cmd --foo=a --foo=b` feels more natural than `cmd --foo=a,b`, isn't it? – imbolc Aug 27 '20 at 09:18
  • Well, I wouldn't say so. There is little precedent for variable-sized sets as arguments, this is in general a sign of improvable cli design. I personally can't remember ever having used `--foo=a --foo=b`. By the way, `--foo a,b` works just as well, without the equal sign. – L. Riemer Aug 27 '20 at 09:23

2 Answers2

1

I've followed the L. Riemer advice, and it seems it's enough to implement FromStr only:

use structopt::StructOpt;

#[derive(Debug, PartialEq)]
struct Foo(Vec<String>);

impl std::str::FromStr for Foo {
    type Err = Box<dyn std::error::Error>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Foo(s.split(",").map(|x| x.trim().to_owned()).collect()))
    }
}

#[derive(StructOpt)]
pub struct Cli {
    #[structopt(long, default_value = "foo, bar")]
    foo: Foo,
}

fn main() {
    let cli = Cli::from_iter(Vec::<String>::new());
    assert_eq!(cli.foo, Foo(vec!["foo".into(), "bar".into()]));

    let cli = Cli::from_iter(vec!["", "--foo", "foo"]);
    assert_eq!(cli.foo, Foo(vec!["foo".into()]));

    let cli = Cli::from_iter(vec!["", "--foo", "foo,bar,baz"]);
    assert_eq!(cli.foo, Foo(vec!["foo".into(), "bar".into(), "baz".into()]));
}
imbolc
  • 1,620
  • 1
  • 19
  • 32
0

I don't think you can do that: while StructOpt has some tricks around default values, I expect this still ends with the default value being injected in the CLI parsing as if it had been provided explicitly, which means there is likely no way to provide multiple default values (though I could certainly be wrong).

You probably want to handle this at the application-level e.g. right after having parsed the CLI, check for the arity of foo and update it if it's empty.

Masklinn
  • 34,759
  • 3
  • 38
  • 57