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.
As outlined by @TeXitoi, I missed the
ArgMatches::indices_of()
function which gives us the required information.