use heck::ToSnakeCase;
use indexmap::IndexMap;
use itertools::Itertools;
use log::trace;
use miette::bail;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::fmt::{Debug, Display, Formatter};
use std::sync::Arc;
use strum::EnumTryAs;
#[cfg(feature = "docs")]
use crate::docs;
use crate::error::UsageErr;
use crate::spec::arg::SpecDoubleDashChoices;
use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
fn get_flag_key(word: &str) -> &str {
if word.starts_with("--") {
word.split_once('=').map(|(k, _)| k).unwrap_or(word)
} else if word.len() >= 2 {
&word[0..2]
} else {
word
}
}
pub struct ParseOutput {
pub cmd: SpecCommand,
pub cmds: Vec<SpecCommand>,
pub args: IndexMap<Arc<SpecArg>, ParseValue>,
pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
pub errors: Vec<UsageErr>,
}
#[derive(Debug, EnumTryAs, Clone)]
pub enum ParseValue {
Bool(bool),
String(String),
MultiBool(Vec<bool>),
MultiString(Vec<String>),
}
#[non_exhaustive]
pub struct Parser<'a> {
spec: &'a Spec,
env: Option<HashMap<String, String>>,
}
impl<'a> Parser<'a> {
pub fn new(spec: &'a Spec) -> Self {
Self { spec, env: None }
}
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
self.env = Some(env);
self
}
pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
let custom_env = self.env.as_ref();
let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
trace!("{out:?}");
let get_env = |key: &str| -> Option<String> {
if let Some(env_map) = custom_env {
env_map.get(key).cloned()
} else {
std::env::var(key).ok()
}
};
for arg in out.cmd.args.iter().skip(out.args.len()) {
if let Some(env_var) = arg.env.as_ref() {
if let Some(env_value) = get_env(env_var) {
validate_choice_value(
ChoiceTarget::arg(arg),
&env_value,
arg.choices.as_ref(),
custom_env,
)?;
out.args
.insert(Arc::new(arg.clone()), ParseValue::String(env_value));
continue;
}
}
if !arg.default.is_empty() {
if arg.var {
validate_choice_values(
ChoiceTarget::arg(arg),
&arg.default,
arg.choices.as_ref(),
custom_env,
)?;
out.args.insert(
Arc::new(arg.clone()),
ParseValue::MultiString(arg.default.clone()),
);
} else {
validate_choice_value(
ChoiceTarget::arg(arg),
&arg.default[0],
arg.choices.as_ref(),
custom_env,
)?;
out.args.insert(
Arc::new(arg.clone()),
ParseValue::String(arg.default[0].clone()),
);
}
}
}
for flag in out.available_flags.values() {
if out.flags.contains_key(flag) {
continue;
}
if let Some(env_var) = flag.env.as_ref() {
if let Some(env_value) = get_env(env_var) {
if let Some(arg) = flag.arg.as_ref() {
validate_choice_value(
ChoiceTarget::option(flag),
&env_value,
arg.choices.as_ref(),
custom_env,
)?;
out.flags
.insert(Arc::clone(flag), ParseValue::String(env_value));
} else {
let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
out.flags
.insert(Arc::clone(flag), ParseValue::Bool(is_true));
}
continue;
}
}
if !flag.default.is_empty() {
if flag.var {
if let Some(arg) = flag.arg.as_ref() {
validate_choice_values(
ChoiceTarget::option(flag),
&flag.default,
arg.choices.as_ref(),
custom_env,
)?;
out.flags.insert(
Arc::clone(flag),
ParseValue::MultiString(flag.default.clone()),
);
} else {
let bools: Vec<bool> = flag
.default
.iter()
.map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
.collect();
out.flags
.insert(Arc::clone(flag), ParseValue::MultiBool(bools));
}
} else {
if let Some(arg) = flag.arg.as_ref() {
validate_choice_value(
ChoiceTarget::option(flag),
&flag.default[0],
arg.choices.as_ref(),
custom_env,
)?;
out.flags.insert(
Arc::clone(flag),
ParseValue::String(flag.default[0].clone()),
);
} else {
let is_true =
matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
out.flags
.insert(Arc::clone(flag), ParseValue::Bool(is_true));
}
}
}
if let Some(arg) = flag.arg.as_ref() {
if !out.flags.contains_key(flag) && !arg.default.is_empty() {
if flag.var {
validate_choice_values(
ChoiceTarget::option(flag),
&arg.default,
arg.choices.as_ref(),
custom_env,
)?;
out.flags.insert(
Arc::clone(flag),
ParseValue::MultiString(arg.default.clone()),
);
} else {
validate_choice_value(
ChoiceTarget::option(flag),
&arg.default[0],
arg.choices.as_ref(),
custom_env,
)?;
out.flags
.insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
}
}
}
}
if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
bail!("{err}");
}
if !out.errors.is_empty() {
bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
}
Ok(out)
}
}
#[must_use = "parsing result should be used"]
pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
Parser::new(spec).parse(input)
}
#[must_use = "parsing result should be used"]
pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
parse_partial_with_env(spec, input, None)
}
fn parse_partial_with_env(
spec: &Spec,
input: &[String],
custom_env: Option<&HashMap<String, String>>,
) -> Result<ParseOutput, miette::Error> {
trace!("parse_partial: {input:?}");
let mut input = input.iter().cloned().collect::<VecDeque<_>>();
input.pop_front();
let gather_flags = |cmd: &SpecCommand| {
cmd.flags
.iter()
.flat_map(|f| {
let f = Arc::new(f.clone()); let mut flags = f
.long
.iter()
.map(|l| (format!("--{l}"), Arc::clone(&f)))
.chain(f.short.iter().map(|s| (format!("-{s}"), Arc::clone(&f))))
.collect::<Vec<_>>();
if let Some(negate) = &f.negate {
flags.push((negate.clone(), Arc::clone(&f)));
}
flags
})
.collect()
};
let mut out = ParseOutput {
cmd: spec.cmd.clone(),
cmds: vec![spec.cmd.clone()],
args: IndexMap::new(),
flags: IndexMap::new(),
available_flags: gather_flags(&spec.cmd),
flag_awaiting_value: vec![],
errors: vec![],
};
let mut prefix_words: Vec<String> = vec![];
let mut idx = 0;
let mut used_default_subcommand = false;
while idx < input.len() {
if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
let mut subcommand = subcommand.clone();
subcommand.mount(&prefix_words)?;
out.available_flags.retain(|_, f| f.global);
out.available_flags.extend(gather_flags(&subcommand));
input.remove(idx);
out.cmds.push(subcommand.clone());
out.cmd = subcommand.clone();
prefix_words.clear();
} else if input[idx].starts_with('-') {
let word = &input[idx];
let flag_key = get_flag_key(word);
if let Some(f) = out.available_flags.get(flag_key) {
if f.global {
prefix_words.push(input[idx].clone());
idx += 1;
if f.arg.is_some()
&& !word.contains('=')
&& idx < input.len()
&& !input[idx].starts_with('-')
{
prefix_words.push(input[idx].clone());
idx += 1;
}
} else {
break;
}
} else {
break;
}
} else {
if !used_default_subcommand {
if let Some(default_name) = &spec.default_subcommand {
if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
let mut subcommand = subcommand.clone();
subcommand.mount(&prefix_words)?;
out.available_flags.retain(|_, f| f.global);
out.available_flags.extend(gather_flags(&subcommand));
out.cmds.push(subcommand.clone());
out.cmd = subcommand.clone();
prefix_words.clear();
used_default_subcommand = true;
continue;
}
}
}
break;
}
}
let mut next_arg = out.cmd.args.first();
let mut enable_flags = true;
let mut grouped_flag = false;
while !input.is_empty() {
let mut w = input.pop_front().unwrap();
if let Some(ref restart_token) = out.cmd.restart_token {
if w == *restart_token {
out.args.clear();
next_arg = out.cmd.args.first();
out.flag_awaiting_value.clear(); enable_flags = true; continue;
}
}
if w == "--" {
enable_flags = false;
let should_preserve = next_arg
.map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
.unwrap_or(false);
if should_preserve {
} else {
continue;
}
}
if enable_flags && w.starts_with("--") {
grouped_flag = false;
let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
if !val.is_empty() {
input.push_front(val.to_string());
}
if let Some(f) = out.available_flags.get(word) {
if f.arg.is_some() {
out.flag_awaiting_value.push(Arc::clone(f));
} else if f.count {
let arr = out
.flags
.entry(Arc::clone(f))
.or_insert_with(|| ParseValue::MultiBool(vec![]))
.try_as_multi_bool_mut()
.unwrap();
arr.push(true);
} else {
let negate = f.negate.clone().unwrap_or_default();
out.flags
.insert(Arc::clone(f), ParseValue::Bool(w != negate));
}
continue;
}
if is_help_arg(spec, &w) {
out.errors
.push(render_help_err(spec, &out.cmd, w.len() > 2));
return Ok(out);
}
}
if enable_flags && w.starts_with('-') && w.len() > 1 {
let short = w.chars().nth(1).unwrap();
if let Some(f) = out.available_flags.get(&format!("-{short}")) {
if w.len() > 2 {
input.push_front(format!("-{}", &w[2..]));
grouped_flag = true;
}
if f.arg.is_some() {
out.flag_awaiting_value.push(Arc::clone(f));
} else if f.count {
let arr = out
.flags
.entry(Arc::clone(f))
.or_insert_with(|| ParseValue::MultiBool(vec![]))
.try_as_multi_bool_mut()
.unwrap();
arr.push(true);
} else {
let negate = f.negate.clone().unwrap_or_default();
out.flags
.insert(Arc::clone(f), ParseValue::Bool(w != negate));
}
continue;
}
if is_help_arg(spec, &w) {
out.errors
.push(render_help_err(spec, &out.cmd, w.len() > 2));
return Ok(out);
}
if grouped_flag {
grouped_flag = false;
w.remove(0);
}
}
if !out.flag_awaiting_value.is_empty() {
while let Some(flag) = out.flag_awaiting_value.pop() {
let arg = flag.arg.as_ref().unwrap();
if validate_choices(
spec,
&out.cmd,
&mut out.errors,
ChoiceTarget::option(&flag),
&w,
arg.choices.as_ref(),
custom_env,
)? {
return Ok(out);
}
if flag.var {
let arr = out
.flags
.entry(flag)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
} else {
out.flags.insert(flag, ParseValue::String(w));
}
w = "".to_string();
}
continue;
}
if let Some(arg) = next_arg {
if validate_choices(
spec,
&out.cmd,
&mut out.errors,
ChoiceTarget::arg(arg),
&w,
arg.choices.as_ref(),
custom_env,
)? {
return Ok(out);
}
if arg.var {
let arr = out
.args
.entry(Arc::new(arg.clone()))
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
next_arg = out.cmd.args.get(out.args.len());
}
} else {
out.args
.insert(Arc::new(arg.clone()), ParseValue::String(w));
next_arg = out.cmd.args.get(out.args.len());
}
continue;
}
if is_help_arg(spec, &w) {
out.errors
.push(render_help_err(spec, &out.cmd, w.len() > 2));
return Ok(out);
}
bail!("unexpected word: {w}");
}
for arg in out.cmd.args.iter().skip(out.args.len()) {
if arg.required && arg.default.is_empty() {
let has_env = arg.env.as_ref().is_some_and(|e| {
custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
|| std::env::var(e).is_ok()
});
if !has_env {
out.errors.push(UsageErr::MissingArg(arg.name.clone()));
}
}
}
for flag in out.available_flags.values() {
if out.flags.contains_key(flag) {
continue;
}
let has_default =
!flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
let has_env = flag.env.as_ref().is_some_and(|e| {
custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
});
if flag.required && !has_default && !has_env {
out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
}
}
for (arg, value) in &out.args {
if arg.var {
if let ParseValue::MultiString(values) = value {
if let Some(min) = arg.var_min {
if values.len() < min {
out.errors.push(UsageErr::VarArgTooFew {
name: arg.name.clone(),
min,
got: values.len(),
});
}
}
if let Some(max) = arg.var_max {
if values.len() > max {
out.errors.push(UsageErr::VarArgTooMany {
name: arg.name.clone(),
max,
got: values.len(),
});
}
}
}
}
}
for (flag, value) in &out.flags {
if flag.var {
let count = match value {
ParseValue::MultiString(values) => values.len(),
ParseValue::MultiBool(values) => values.len(),
_ => continue,
};
if let Some(min) = flag.var_min {
if count < min {
out.errors.push(UsageErr::VarFlagTooFew {
name: flag.name.clone(),
min,
got: count,
});
}
}
if let Some(max) = flag.var_max {
if count > max {
out.errors.push(UsageErr::VarFlagTooMany {
name: flag.name.clone(),
max,
got: count,
});
}
}
}
}
Ok(out)
}
#[cfg(feature = "docs")]
fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
UsageErr::Help(docs::cli::render_help(spec, cmd, long))
}
#[cfg(not(feature = "docs"))]
fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
UsageErr::Help("help".to_string())
}
#[derive(Copy, Clone)]
struct ChoiceTarget<'a> {
kind: &'a str,
name: &'a str,
}
impl<'a> ChoiceTarget<'a> {
fn arg(arg: &'a SpecArg) -> Self {
Self {
kind: "arg",
name: &arg.name,
}
}
fn option(flag: &'a SpecFlag) -> Self {
Self {
kind: "option",
name: &flag.name,
}
}
}
fn choice_error(
target: ChoiceTarget<'_>,
value: &str,
choices: Option<&SpecChoices>,
custom_env: Option<&HashMap<String, String>>,
) -> Option<String> {
let choices = choices?;
let values = choices.values_with_env(custom_env);
if values.iter().any(|choice| choice == value) {
return None;
}
if let Some(env) = choices.env() {
if values.is_empty() {
return Some(format!(
"Invalid choice for {} {}: {value}, no choices resolved from env {env}",
target.kind, target.name,
));
}
}
Some(format!(
"Invalid choice for {} {}: {value}, expected one of {}",
target.kind,
target.name,
values.join(", ")
))
}
fn validate_choices(
spec: &Spec,
cmd: &SpecCommand,
errors: &mut Vec<UsageErr>,
target: ChoiceTarget<'_>,
value: &str,
choices: Option<&SpecChoices>,
custom_env: Option<&HashMap<String, String>>,
) -> miette::Result<bool> {
if is_help_arg(spec, value)
&& choices.is_some_and(|choices| {
!choices
.values_with_env(custom_env)
.iter()
.any(|choice| choice == value)
})
{
errors.push(render_help_err(spec, cmd, value.len() > 2));
return Ok(true);
}
if let Some(err) = choice_error(target, value, choices, custom_env) {
bail!("{err}");
}
Ok(false)
}
fn validate_choice_value(
target: ChoiceTarget<'_>,
value: &str,
choices: Option<&SpecChoices>,
custom_env: Option<&HashMap<String, String>>,
) -> miette::Result<()> {
if let Some(err) = choice_error(target, value, choices, custom_env) {
bail!("{err}");
}
Ok(())
}
fn validate_choice_values(
target: ChoiceTarget<'_>,
values: &[String],
choices: Option<&SpecChoices>,
custom_env: Option<&HashMap<String, String>>,
) -> miette::Result<()> {
for value in values {
validate_choice_value(target, value, choices, custom_env)?;
}
Ok(())
}
fn is_help_arg(spec: &Spec, w: &str) -> bool {
spec.disable_help != Some(true)
&& (w == "--help"
|| w == "-h"
|| w == "-?"
|| (spec.cmd.subcommands.is_empty() && w == "help"))
}
impl ParseOutput {
pub fn as_env(&self) -> BTreeMap<String, String> {
let mut env = BTreeMap::new();
for (flag, val) in &self.flags {
let key = format!("usage_{}", flag.name.to_snake_case());
let val = match val {
ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
ParseValue::String(s) => s.clone(),
ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
ParseValue::MultiString(s) => shell_words::join(s),
};
env.insert(key, val);
}
for (arg, val) in &self.args {
let key = format!("usage_{}", arg.name.to_snake_case());
env.insert(key, val.to_string());
}
env
}
}
impl Display for ParseValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseValue::Bool(b) => write!(f, "{b}"),
ParseValue::String(s) => write!(f, "{s}"),
ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
}
}
}
impl Debug for ParseOutput {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParseOutput")
.field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
.field(
"args",
&self
.args
.iter()
.map(|(a, w)| format!("{}: {w}", &a.name))
.collect_vec(),
)
.field(
"available_flags",
&self
.available_flags
.iter()
.map(|(f, w)| format!("{f}: {w}"))
.collect_vec(),
)
.field(
"flags",
&self
.flags
.iter()
.map(|(f, w)| format!("{}: {w}", &f.name))
.collect_vec(),
)
.field("flag_awaiting_value", &self.flag_awaiting_value)
.field("errors", &self.errors)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn input(words: &[&str]) -> Vec<String> {
words.iter().map(|word| (*word).to_string()).collect()
}
fn spec_with_arg(arg: SpecArg) -> Spec {
let cmd = SpecCommand::builder().name("test").arg(arg).build();
Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
}
}
fn spec_with_flag(flag: SpecFlag) -> Spec {
let cmd = SpecCommand::builder().name("test").flag(flag).build();
Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
}
}
fn parse_with_env(
spec: &Spec,
words: &[&str],
env: &[(&str, &str)],
) -> Result<ParseOutput, miette::Error> {
let env = env
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
Parser::new(spec).with_env(env).parse(&input(words))
}
fn first_string_value(parsed: &ParseOutput) -> &str {
if let Some(ParseValue::String(value)) = parsed.args.values().next() {
return value;
}
if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
return value;
}
panic!("expected first parsed value to be ParseValue::String");
}
fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
let err = result.expect_err("expected parser error");
assert_eq!(format!("{err}"), expected);
}
#[cfg(feature = "unstable_choices_env")]
fn spec_arg_choices_env(key: &str) -> Spec {
spec_with_arg(
SpecArg::builder()
.name("env")
.choices_env(key)
.required(false)
.build(),
)
}
#[cfg(feature = "unstable_choices_env")]
fn spec_flag_choices_env(key: &str) -> Spec {
spec_with_flag(
SpecFlag::builder()
.long("env")
.arg(SpecArg::builder().name("env").choices_env(key).build())
.build(),
)
}
#[test]
fn test_parse() {
let cmd = SpecCommand::builder()
.name("test")
.arg(SpecArg::builder().name("arg").build())
.flag(SpecFlag::builder().long("flag").build())
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 1);
assert_eq!(parsed.cmds[0].name, "test");
assert_eq!(parsed.args.len(), 1);
assert_eq!(parsed.flags.len(), 1);
assert_eq!(parsed.available_flags.len(), 1);
}
#[test]
fn test_as_env() {
let cmd = SpecCommand::builder()
.name("test")
.arg(SpecArg::builder().name("arg").build())
.flag(SpecFlag::builder().long("flag").build())
.flag(
SpecFlag::builder()
.long("force")
.negate("--no-force")
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"--flag".to_string(),
"--no-force".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
let env = parsed.as_env();
assert_eq!(env.len(), 2);
assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
}
#[test]
fn test_arg_env_var() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("input")
.env("TEST_ARG_INPUT")
.required(true)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let arg = parsed.args.keys().next().unwrap();
assert_eq!(arg.name, "input");
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "test_file.txt");
std::env::remove_var("TEST_ARG_INPUT");
}
#[test]
fn test_flag_env_var_with_arg() {
let cmd = SpecCommand::builder()
.name("test")
.flag(
SpecFlag::builder()
.long("output")
.env("TEST_FLAG_OUTPUT")
.arg(SpecArg::builder().name("file").build())
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.flags.len(), 1);
let flag = parsed.flags.keys().next().unwrap();
assert_eq!(flag.name, "output");
let value = parsed.flags.values().next().unwrap();
assert_eq!(value.to_string(), "output.txt");
std::env::remove_var("TEST_FLAG_OUTPUT");
}
#[test]
fn test_flag_env_var_boolean() {
let cmd = SpecCommand::builder()
.name("test")
.flag(
SpecFlag::builder()
.long("verbose")
.env("TEST_FLAG_VERBOSE")
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
std::env::set_var("TEST_FLAG_VERBOSE", "true");
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.flags.len(), 1);
let flag = parsed.flags.keys().next().unwrap();
assert_eq!(flag.name, "verbose");
let value = parsed.flags.values().next().unwrap();
assert_eq!(value.to_string(), "true");
std::env::remove_var("TEST_FLAG_VERBOSE");
}
#[test]
fn test_env_var_precedence() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("input")
.env("TEST_PRECEDENCE_INPUT")
.required(true)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
let input = vec!["test".to_string(), "cli_file.txt".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "cli_file.txt");
std::env::remove_var("TEST_PRECEDENCE_INPUT");
}
#[test]
fn test_flag_var_true_with_single_default() {
let cmd = SpecCommand::builder()
.name("test")
.flag(
SpecFlag::builder()
.long("foo")
.var(true)
.arg(SpecArg::builder().name("foo").build())
.default_value("bar")
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.flags.len(), 1);
let flag = parsed.flags.keys().next().unwrap();
assert_eq!(flag.name, "foo");
let value = parsed.flags.values().next().unwrap();
match value {
ParseValue::MultiString(v) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0], "bar");
}
_ => panic!("Expected MultiString, got {:?}", value),
}
}
#[test]
fn test_flag_var_true_with_multiple_defaults() {
let cmd = SpecCommand::builder()
.name("test")
.flag(
SpecFlag::builder()
.long("foo")
.var(true)
.arg(SpecArg::builder().name("foo").build())
.default_values(["xyz", "bar"])
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.flags.len(), 1);
let value = parsed.flags.values().next().unwrap();
match value {
ParseValue::MultiString(v) => {
assert_eq!(v.len(), 2);
assert_eq!(v[0], "xyz");
assert_eq!(v[1], "bar");
}
_ => panic!("Expected MultiString, got {:?}", value),
}
}
#[test]
fn test_flag_var_false_with_default_remains_string() {
let cmd = SpecCommand::builder()
.name("test")
.flag(
SpecFlag::builder()
.long("foo")
.var(false) .arg(SpecArg::builder().name("foo").build())
.default_value("bar")
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.flags.len(), 1);
let value = parsed.flags.values().next().unwrap();
match value {
ParseValue::String(s) => {
assert_eq!(s, "bar");
}
_ => panic!("Expected String, got {:?}", value),
}
}
#[test]
fn test_arg_var_true_with_single_default() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("files")
.var(true)
.default_value("default.txt")
.required(false)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
match value {
ParseValue::MultiString(v) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0], "default.txt");
}
_ => panic!("Expected MultiString, got {:?}", value),
}
}
#[test]
fn test_arg_var_true_with_multiple_defaults() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("files")
.var(true)
.default_values(["file1.txt", "file2.txt"])
.required(false)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
match value {
ParseValue::MultiString(v) => {
assert_eq!(v.len(), 2);
assert_eq!(v[0], "file1.txt");
assert_eq!(v[1], "file2.txt");
}
_ => panic!("Expected MultiString, got {:?}", value),
}
}
#[test]
fn test_arg_var_false_with_default_remains_string() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("file")
.var(false)
.default_value("default.txt")
.required(false)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec!["test".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
match value {
ParseValue::String(s) => {
assert_eq!(s, "default.txt");
}
_ => panic!("Expected String, got {:?}", value),
}
}
#[test]
fn test_scalar_defaults_validate_only_first_default_choice() {
let specs = [
spec_with_arg(
SpecArg::builder()
.name("env")
.var(false)
.default_values(["dev", "prod"])
.choices(["dev"])
.required(false)
.build(),
),
spec_with_flag(
SpecFlag::builder()
.long("env")
.arg(
SpecArg::builder()
.name("env")
.default_values(["dev", "prod"])
.choices(["dev"])
.build(),
)
.build(),
),
];
for spec in specs {
let parsed = parse(&spec, &input(&["test"])).unwrap();
assert_eq!(first_string_value(&parsed), "dev");
}
}
#[test]
fn test_default_subcommand() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};
let input = vec!["test".to_string(), "mytask".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 2);
assert_eq!(parsed.cmds[1].name, "run");
assert_eq!(parsed.args.len(), 1);
let arg = parsed.args.keys().next().unwrap();
assert_eq!(arg.name, "task");
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "mytask");
}
#[test]
fn test_default_subcommand_explicit_still_works() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.build();
let other_cmd = SpecCommand::builder()
.name("other")
.arg(SpecArg::builder().name("other_arg").build())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
cmd.subcommands.insert("other".to_string(), other_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};
let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 2);
assert_eq!(parsed.cmds[1].name, "other");
}
#[test]
fn test_default_subcommand_with_nested_subcommands() {
let say_cmd = SpecCommand::builder()
.name("say")
.arg(SpecArg::builder().name("name").build())
.build();
let mut run_cmd = SpecCommand::builder().name("run").build();
run_cmd.subcommands.insert("say".to_string(), say_cmd);
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};
let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 3);
assert_eq!(parsed.cmds[0].name, "test");
assert_eq!(parsed.cmds[1].name, "run");
assert_eq!(parsed.cmds[2].name, "say");
assert_eq!(parsed.args.len(), 1);
let arg = parsed.args.keys().next().unwrap();
assert_eq!(arg.name, "name");
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "hello");
}
#[test]
fn test_default_subcommand_same_name_child() {
let run_task = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("args").build())
.build();
let mut run_cmd = SpecCommand::builder().name("run").build();
run_cmd.subcommands.insert("run".to_string(), run_task);
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};
let input = vec!["test".to_string(), "run".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 2);
assert_eq!(parsed.cmds[0].name, "test");
assert_eq!(parsed.cmds[1].name, "run");
let input = vec![
"test".to_string(),
"run".to_string(),
"run".to_string(),
"hello".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 3);
assert_eq!(parsed.cmds[0].name, "test");
assert_eq!(parsed.cmds[1].name, "run");
assert_eq!(parsed.cmds[2].name, "run");
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "hello");
let mut run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.build();
let run_task = SpecCommand::builder().name("run").build();
run_cmd.subcommands.insert("run".to_string(), run_task);
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};
let input = vec!["test".to_string(), "other".to_string()];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.cmds.len(), 2);
assert_eq!(parsed.cmds[0].name, "test");
assert_eq!(parsed.cmds[1].name, "run");
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "other");
}
#[test]
fn test_restart_token() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.restart_token(":::".to_string())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
":::".to_string(),
"task2".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "task2");
}
#[test]
fn test_restart_token_multiple() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.restart_token(":::".to_string())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
":::".to_string(),
"task2".to_string(),
":::".to_string(),
"task3".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "task3");
}
#[test]
fn test_restart_token_clears_flag_awaiting_value() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.flag(
SpecFlag::builder()
.name("jobs")
.long("jobs")
.arg(SpecArg::builder().name("count").build())
.build(),
)
.restart_token(":::".to_string())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
"--jobs".to_string(),
":::".to_string(),
"task2".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "task2");
assert!(parsed.flag_awaiting_value.is_empty());
}
#[test]
fn test_restart_token_resets_double_dash() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.arg(SpecArg::builder().name("extra_args").var(true).build())
.flag(SpecFlag::builder().name("verbose").long("verbose").build())
.restart_token(":::".to_string())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
"--".to_string(),
"extra".to_string(),
":::".to_string(),
"--verbose".to_string(),
"task2".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
let value = parsed.args.get(task_arg).unwrap();
assert_eq!(value.to_string(), "task2");
}
#[test]
fn test_double_dashes_without_preserve() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("args").var(true).build())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"arg1".to_string(),
"--".to_string(),
"arg2".to_string(),
"--".to_string(),
"arg3".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
let value = parsed.args.get(args_arg).unwrap();
assert_eq!(value.to_string(), "arg1 arg2 arg3");
}
#[test]
fn test_double_dashes_with_preserve() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(
SpecArg::builder()
.name("args")
.var(true)
.double_dash(SpecDoubleDashChoices::Preserve)
.build(),
)
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"arg1".to_string(),
"--".to_string(),
"arg2".to_string(),
"--".to_string(),
"arg3".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
let value = parsed.args.get(args_arg).unwrap();
assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
}
#[test]
fn test_double_dashes_with_preserve_only_dashes() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(
SpecArg::builder()
.name("args")
.var(true)
.double_dash(SpecDoubleDashChoices::Preserve)
.build(),
)
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"--".to_string(),
"--".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
let value = parsed.args.get(args_arg).unwrap();
assert_eq!(value.to_string(), "-- --");
}
#[test]
fn test_double_dashes_with_preserve_multiple_args() {
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.arg(
SpecArg::builder()
.name("extra_args")
.var(true)
.double_dash(SpecDoubleDashChoices::Preserve)
.build(),
)
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
"--".to_string(),
"arg1".to_string(),
"--".to_string(),
"--foo".to_string(),
];
let parsed = parse(&spec, &input).unwrap();
let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
let task_value = parsed.args.get(task_arg).unwrap();
assert_eq!(task_value.to_string(), "task1");
let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
let extra_value = parsed.args.get(extra_arg).unwrap();
assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
}
#[test]
fn test_parser_with_custom_env_for_required_arg() {
let spec = spec_with_arg(
SpecArg::builder()
.name("name")
.env("NAME")
.required(true)
.build(),
);
std::env::remove_var("NAME");
let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
.expect("parse should succeed with custom env");
assert_eq!(parsed.args.len(), 1);
assert_eq!(first_string_value(&parsed), "john");
}
#[test]
fn test_parser_with_custom_env_for_required_flag() {
let spec = spec_with_flag(
SpecFlag::builder()
.long("name")
.env("NAME")
.required(true)
.arg(SpecArg::builder().name("name").build())
.build(),
);
std::env::remove_var("NAME");
let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
.expect("parse should succeed with custom env");
assert_eq!(parsed.flags.len(), 1);
assert_eq!(first_string_value(&parsed), "jane");
}
#[test]
fn test_parser_with_custom_env_still_fails_when_missing() {
let spec = spec_with_arg(
SpecArg::builder()
.name("name")
.env("NAME")
.required(true)
.build(),
);
std::env::remove_var("NAME");
assert!(parse_with_env(&spec, &["test"], &[]).is_err());
}
#[test]
fn test_parser_does_not_treat_env_choice_value_as_help() {
let spec = spec_with_arg(
SpecArg::builder()
.name("env")
.env("CURRENT_ENV")
.choices(["dev", "staging"])
.required(false)
.build(),
);
assert_parse_err(
parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
"Invalid choice for arg env: --help, expected one of dev, staging",
);
}
#[test]
fn test_parser_does_not_treat_default_choice_value_as_help() {
let spec = spec_with_flag(
SpecFlag::builder()
.long("env")
.arg(
SpecArg::builder()
.name("env")
.choices(["dev", "staging"])
.build(),
)
.default_value("--help")
.build(),
);
assert_parse_err(
parse_with_env(&spec, &["test"], &[]),
"Invalid choice for option env: --help, expected one of dev, staging",
);
}
#[cfg(feature = "unstable_choices_env")]
#[test]
fn test_parser_arg_choices_from_custom_env() {
let spec = spec_arg_choices_env("DEPLOY_ENVS");
let parsed =
parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
assert_eq!(first_string_value(&parsed), "bar");
assert_parse_err(
parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
"Invalid choice for arg env: prod, expected one of foo, bar, baz",
);
assert_parse_err(
parse_with_env(&spec, &["test", "prod"], &[]),
"Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
);
}
#[cfg(feature = "unstable_choices_env")]
#[test]
fn test_parser_validates_flag_choices_from_custom_env() {
let spec = spec_flag_choices_env("DEPLOY_ENVS");
let parsed = parse_with_env(
&spec,
&["test", "--env", "baz"],
&[("DEPLOY_ENVS", "foo,bar baz")],
)
.unwrap();
assert_eq!(first_string_value(&parsed), "baz");
}
#[cfg(feature = "unstable_choices_env")]
#[test]
fn test_parser_revalidates_env_and_default_values_against_choices_env() {
let arg_env_spec = spec_with_arg(
SpecArg::builder()
.name("env")
.env("CURRENT_ENV")
.choices_env("DEPLOY_ENVS")
.build(),
);
assert_parse_err(
parse_with_env(
&arg_env_spec,
&["test"],
&[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
),
"Invalid choice for arg env: prod, expected one of dev, staging",
);
let flag_default_spec = spec_with_flag(
SpecFlag::builder()
.long("env")
.arg(
SpecArg::builder()
.name("env")
.choices_env("DEPLOY_ENVS")
.build(),
)
.default_value("prod")
.build(),
);
assert_parse_err(
parse_with_env(
&flag_default_spec,
&["test"],
&[("DEPLOY_ENVS", "dev,staging")],
),
"Invalid choice for option env: prod, expected one of dev, staging",
);
}
#[test]
fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
let spec: Spec = r#"
flag "-v --verbose" var=#true
arg "[database]" default="myapp_dev"
arg "[args...]"
"#
.parse()
.unwrap();
let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
let env = parsed.as_env();
assert_eq!(env.get("usage_database").unwrap(), "mydb");
assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
}
#[test]
fn test_variadic_arg_captures_unknown_flags() {
let cmd = SpecCommand::builder()
.name("test")
.flag(SpecFlag::builder().short('v').long("verbose").build())
.arg(SpecArg::builder().name("database").required(false).build())
.arg(
SpecArg::builder()
.name("args")
.required(false)
.var(true)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 2);
let args_val = parsed
.args
.iter()
.find(|(a, _)| a.name == "args")
.unwrap()
.1;
match args_val {
ParseValue::MultiString(v) => {
assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
}
_ => panic!("Expected MultiString, got {:?}", args_val),
}
}
#[test]
fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
let cmd = SpecCommand::builder()
.name("test")
.flag(SpecFlag::builder().short('v').long("verbose").build())
.arg(SpecArg::builder().name("database").required(false).build())
.arg(
SpecArg::builder()
.name("args")
.required(false)
.var(true)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.args.len(), 2);
let args_val = parsed
.args
.iter()
.find(|(a, _)| a.name == "args")
.unwrap()
.1;
match args_val {
ParseValue::MultiString(v) => {
assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
}
_ => panic!("Expected MultiString, got {:?}", args_val),
}
}
}