milend
milend

Reputation: 71

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

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:

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.

Upvotes: 4

Views: 780

Answers (1)

milend
milend

Reputation: 71

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

Upvotes: 3

Related Questions