Reputation: 71
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.
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)>,
}
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:
--config
or --config-file
(it's not always possible to figure out how a value was passed just by inspecting the value).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>,
}
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
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