Getting relative order of different command line options using clap & structopt

674 Views Asked by At

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.

1

There are 1 best solutions below

0
On

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);
    }
}