3

The Problem

I have a command that takes different options and the relative order of those options is important to the semantics of the command. For example, in command --config A --some-option --config-file B --random-option --config C --another-option --more-options --config-file D, the relative order of A, B, C, D is important as it affects the meaning of the command.

If I just define the options as follows:

#[derive(Debug, StructOpt)]
pub struct Command {
    #[structopt(long = "config")]
    configs: Vec<String>,

    #[structopt(long = "config-file")]
    config_files: Vec<String>,
}

Then I will get two vectors, configs = [A, C] and config_files = [B, D] but the relative order between the elements in configs and config_files has been lost.

Ideas

Custom Parse Functions

The idea was to provide a custom parse function and use a counter to record the indexes as each option was parsed. Unfortunately, the parsing functions are not called in the original order defined by the command.

fn get_next_atomic_int() -> usize {
    static ATOMIC_COUNTER: Lazy<AtomicUsize> = Lazy::new(|| AtomicUsize::new(0));
    ATOMIC_COUNTER.fetch_add(1, Ordering::Relaxed)
}

fn parse_passthrough_string_ordered(arg: &str) -> (String, usize) {
    (arg.to_owned(), get_next_atomic_int())
}

#[derive(Debug, StructOpt)]
#[structopt(name = "command"]
pub struct Command {
    #[structopt(long = "config-file", parse(from_str = parse_passthrough_string_ordered))]
    config_files: Vec<(String, usize)>,
    
    #[structopt(short = "c", long = "config", parse(from_str = parse_passthrough_string_ordered))]
    configs: Vec<(String, usize)>,
}

Aliases

I can add an alias for the option, like so:

#[derive(Debug, StructOpt)]
pub struct Command {
    #[structopt(long = "config", visible_alias = "config-file")]
    configs: Vec<String>,
}

There are two problems with this approach:

  • I need a way to distinguish whether an option was passed via --config or --config-file (it's not always possible to figure out how a value was passed just by inspecting the value).
  • I cannot provide a short option for the visible alias.

Same Vector, Multiple Options

The other idea was to attach multiple structopt directives, so that the same underlying vector would be used for both options. Unfortunately, it does not work - structopt only uses the last directive. Something like:

#[derive(Debug)]
enum Config {
    File(String),
    Literal(String),
}

fn parse_config_literal(arg: &str) -> Config {
    Config::Literal(arg.to_owned())
}

fn parse_config_file(arg: &str) -> Config {
    Config::File(arg.to_owned())
}

#[derive(Debug, StructOpt)]
#[structopt(name = "example")]
struct Opt {
    #[structopt(long = "--config-file", parse(from_str = parse_config_file))]
    #[structopt(short = "-c", long = "--config", parse(from_str = parse_config_literal))]
    options: Vec<Config>,
}

Recovering the Order

I could try to recover the original order by searching for the parsed values. But this means I would have to duplicate quite a bit of parsing logic (e.g., need to support passing --config=X, --config X, need to handle X appearing as input to another option, etc).

I'd rather just have a way to reliably get the original rather rather than lose the order and try to recover it in a possibly fragile way.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
milend
  • 61
  • 4
  • Does every `--config` option need to be followed by a `--config-file`? If so, can you define that `--config` needs two parameters, and skip the `--config-file` part? – John Bayko Apr 27 '21 at 21:49
  • @JohnBayko: Nope, it's just an example. There are no constraints on how many such options appear (e.g., only `--config`, only `--config-file`, both or even intermixed with other ones). For example, `--config X --some-other-option --config Y --another --verbose --config-file Z --debug --config M`. – milend Apr 28 '21 at 00:55

1 Answers1

3

As outlined by @TeXitoi, I missed the ArgMatches::indices_of() function which gives us the required information.

use structopt::StructOpt;

#[derive(Debug)]
enum Config {
    File(String),
    Literal(String),
}

fn parse_config_literal(arg: &str) -> Config {
    Config::Literal(arg.to_owned())
}

fn parse_config_file(arg: &str) -> Config {
    Config::File(arg.to_owned())
}

#[derive(Debug, StructOpt)]
#[structopt(name = "example")]
struct Opt {
    #[structopt(short = "c", long = "config", parse(from_str = parse_config_literal))]
    configs: Vec<Config>,

    #[structopt(long = "config-file", parse(from_str = parse_config_file))]
    config_files: Vec<Config>,
}

fn with_indices<'a, I: IntoIterator + 'a>(
    collection: I,
    name: &str,
    matches: &'a structopt::clap::ArgMatches,
) -> impl Iterator<Item = (usize, I::Item)> + 'a {
    matches
        .indices_of(name)
        .into_iter()
        .flatten()
        .zip(collection)
}

fn main() {
    let args = vec!["example", "--config", "A", "--config-file", "B", "--config", "C", "--config-file", "D"];
    
    let clap = Opt::clap();
    let matches = clap.get_matches_from(args);
    let opt = Opt::from_clap(&matches);

    println!("configs:");
    for (i, c) in with_indices(&opt.configs, "configs", &matches) {
        println!("{}: {:#?}", i, c);
    }

    println!("\nconfig-files:");
    for (i, c) in with_indices(&opt.config_files, "config-files", &matches) {
        println!("{}: {:#?}", i, c);
    }
}
milend
  • 61
  • 4