use std::collections::{HashMap, HashSet, VecDeque};
use crate::error::ClickError;
type TokenNormalizeFunc = Box<dyn Fn(&str) -> String + Send + Sync>;
pub type ParseResult = Result<(HashMap<String, ParsedValue>, Vec<String>, Vec<String>), ClickError>;
pub const NARGS_OPTIONAL: i32 = -2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OptionAction {
#[default]
Store,
StoreConst,
Append,
AppendConst,
Count,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParsedValue {
Single(String),
Multiple(Vec<String>),
Count(usize),
Flag(bool),
Unset,
FlagNeedsValue,
}
impl ParsedValue {
pub fn is_unset(&self) -> bool {
matches!(self, ParsedValue::Unset)
}
pub fn as_single(&self) -> Option<&str> {
match self {
ParsedValue::Single(s) => Some(s),
_ => None,
}
}
pub fn as_multiple(&self) -> Option<&[String]> {
match self {
ParsedValue::Multiple(v) => Some(v),
_ => None,
}
}
pub fn as_count(&self) -> Option<usize> {
match self {
ParsedValue::Count(n) => Some(*n),
_ => None,
}
}
pub fn as_flag(&self) -> Option<bool> {
match self {
ParsedValue::Flag(b) => Some(*b),
_ => None,
}
}
}
#[derive(Debug, Clone)]
struct ParserOption {
#[allow(dead_code)]
name: String,
#[allow(dead_code)]
short_opts: Vec<String>,
#[allow(dead_code)]
long_opts: Vec<String>,
action: OptionAction,
nargs: i32,
const_value: Option<String>,
dest: String,
flag_needs_value: bool,
}
impl ParserOption {
fn takes_value(&self) -> bool {
matches!(self.action, OptionAction::Store | OptionAction::Append)
}
}
#[derive(Debug, Clone)]
struct ParserArgument {
#[allow(dead_code)]
name: String,
nargs: i32,
dest: String,
}
#[derive(Debug)]
struct ParsingState {
opts: HashMap<String, ParsedValue>,
largs: Vec<String>,
rargs: VecDeque<String>,
order: Vec<String>,
}
impl ParsingState {
fn new(args: Vec<String>) -> Self {
Self {
opts: HashMap::new(),
largs: Vec::new(),
rargs: VecDeque::from(args),
order: Vec::new(),
}
}
}
pub fn split_opt(opt: &str) -> Option<(&str, &str)> {
if opt.is_empty() {
return None;
}
let first = opt.chars().next().unwrap();
if first.is_alphanumeric() {
return None;
}
if opt.len() >= 2 && &opt[0..2] == "--" {
let name = &opt[2..];
if name.is_empty() {
return None; }
return Some(("--", name));
}
if opt.len() >= 2 && opt.starts_with('-') {
return Some(("-", &opt[1..]));
}
if opt.len() >= 2 {
let prefix_end = first.len_utf8();
Some((&opt[..prefix_end], &opt[prefix_end..]))
} else {
None
}
}
fn edit_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
if m == 0 {
return n;
}
if n == 0 {
return m;
}
let mut prev = vec![0usize; n + 1];
let mut curr = vec![0usize; n + 1];
for (j, item) in prev.iter_mut().enumerate().take(n + 1) {
*item = j;
}
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
curr[j] = (prev[j] + 1) .min(curr[j - 1] + 1) .min(prev[j - 1] + cost); }
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
fn get_close_matches(word: &str, possibilities: &[&str], max_matches: usize) -> Vec<String> {
let mut scored: Vec<(usize, &str)> = possibilities
.iter()
.map(|&p| (edit_distance(word, p), p))
.filter(|(dist, _)| *dist <= 2)
.collect();
scored.sort_by_key(|(dist, _)| *dist);
scored
.into_iter()
.take(max_matches)
.map(|(_, s)| s.to_string())
.collect()
}
fn unpack_args(
args: &[String],
nargs_spec: &[i32],
) -> Result<(Vec<ParsedValue>, Vec<String>), ClickError> {
let mut args_deque: VecDeque<String> = VecDeque::from(args.to_vec());
let mut nargs_deque: VecDeque<i32> = VecDeque::from(nargs_spec.to_vec());
let mut result: Vec<ParsedValue> = Vec::new();
let mut star_pos: Option<usize> = None;
fn fetch(deque: &mut VecDeque<String>, from_back: bool) -> Option<String> {
if from_back {
deque.pop_back()
} else {
deque.pop_front()
}
}
fn count_required_remaining(deque: &VecDeque<i32>) -> usize {
deque
.iter()
.filter(|&&n| n > 0) .map(|&n| n.max(1) as usize)
.sum()
}
while let Some(nargs) = if star_pos.is_none() {
nargs_deque.pop_front()
} else {
nargs_deque.pop_back()
} {
if nargs == 1 {
let value = fetch(&mut args_deque, star_pos.is_some());
result.push(match value {
Some(v) => ParsedValue::Single(v),
None => ParsedValue::Unset,
});
} else if nargs == NARGS_OPTIONAL {
let required_remaining = count_required_remaining(&nargs_deque);
let available = args_deque.len();
if available > required_remaining {
let value = fetch(&mut args_deque, star_pos.is_some());
result.push(match value {
Some(v) => ParsedValue::Single(v),
None => ParsedValue::Unset,
});
} else {
result.push(ParsedValue::Unset);
}
} else if nargs > 1 {
let mut values = Vec::new();
let mut missing_count = 0;
for _ in 0..nargs {
match fetch(&mut args_deque, star_pos.is_some()) {
Some(v) => {
values.push(v);
}
None => {
missing_count += 1;
}
}
}
if star_pos.is_some() {
values.reverse();
}
if missing_count == nargs as usize {
result.push(ParsedValue::Unset);
} else if missing_count > 0 {
result.push(ParsedValue::Multiple(values));
} else {
result.push(ParsedValue::Multiple(values));
}
} else if nargs == -1 {
if star_pos.is_some() {
return Err(ClickError::usage(
"Cannot have more than one variadic argument.".to_string()
));
}
star_pos = Some(result.len());
result.push(ParsedValue::Unset); }
}
if let Some(pos) = star_pos {
let remaining: Vec<String> = args_deque.drain(..).collect();
if remaining.is_empty() {
result[pos] = ParsedValue::Multiple(Vec::new());
} else {
result[pos] = ParsedValue::Multiple(remaining);
}
let after_star: Vec<_> = result.drain(pos + 1..).rev().collect();
result.extend(after_star);
}
Ok((result, args_deque.into_iter().collect()))
}
pub struct OptionParser {
allow_interspersed_args: bool,
ignore_unknown_options: bool,
short_opt: HashMap<String, ParserOption>,
long_opt: HashMap<String, ParserOption>,
opt_prefixes: HashSet<String>,
args: Vec<ParserArgument>,
token_normalize_func: Option<TokenNormalizeFunc>,
}
impl std::fmt::Debug for OptionParser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OptionParser")
.field("allow_interspersed_args", &self.allow_interspersed_args)
.field("ignore_unknown_options", &self.ignore_unknown_options)
.field("short_opt", &self.short_opt)
.field("long_opt", &self.long_opt)
.field("opt_prefixes", &self.opt_prefixes)
.field("args", &self.args)
.field(
"token_normalize_func",
&self.token_normalize_func.as_ref().map(|_| "<function>"),
)
.finish()
}
}
impl Default for OptionParser {
fn default() -> Self {
Self::new()
}
}
impl OptionParser {
pub fn new() -> Self {
let mut opt_prefixes = HashSet::new();
opt_prefixes.insert("-".to_string());
opt_prefixes.insert("--".to_string());
Self {
allow_interspersed_args: true,
ignore_unknown_options: false,
short_opt: HashMap::new(),
long_opt: HashMap::new(),
opt_prefixes,
args: Vec::new(),
token_normalize_func: None,
}
}
pub fn allow_interspersed_args(mut self, allow: bool) -> Self {
self.allow_interspersed_args = allow;
self
}
pub fn ignore_unknown_options(mut self, ignore: bool) -> Self {
self.ignore_unknown_options = ignore;
self
}
pub fn token_normalize_func<F>(mut self, func: F) -> Self
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
self.token_normalize_func = Some(Box::new(func));
self
}
fn normalize_opt(&self, opt: &str) -> String {
if let Some(ref func) = self.token_normalize_func {
if let Some((prefix, name)) = split_opt(opt) {
format!("{}{}", prefix, func(name))
} else {
opt.to_string()
}
} else {
opt.to_string()
}
}
pub fn add_option(
&mut self,
opts: &[&str],
dest: &str,
action: OptionAction,
nargs: i32,
const_value: Option<&str>,
) {
self.add_option_ex(opts, dest, action, nargs, const_value, false);
}
pub fn add_option_ex(
&mut self,
opts: &[&str],
dest: &str,
action: OptionAction,
nargs: i32,
const_value: Option<&str>,
flag_needs_value: bool,
) {
let opts: Vec<String> = opts.iter().map(|o| self.normalize_opt(o)).collect();
let mut short_opts = Vec::new();
let mut long_opts = Vec::new();
for opt in &opts {
if let Some((prefix, value)) = split_opt(opt) {
self.opt_prefixes.insert(prefix.chars().next().unwrap().to_string());
if prefix.len() == 2 {
self.opt_prefixes.insert(prefix.to_string());
}
if prefix.len() == 1 && value.len() == 1 {
short_opts.push(opt.clone());
} else {
long_opts.push(opt.clone());
}
}
}
let name = long_opts
.first()
.or(short_opts.first())
.cloned()
.unwrap_or_else(|| dest.to_string());
let parser_opt = ParserOption {
name,
short_opts: short_opts.clone(),
long_opts: long_opts.clone(),
action,
nargs,
const_value: const_value.map(String::from),
dest: dest.to_string(),
flag_needs_value,
};
for opt in &short_opts {
self.short_opt.insert(opt.clone(), parser_opt.clone());
}
for opt in &long_opts {
self.long_opt.insert(opt.clone(), parser_opt.clone());
}
}
pub fn add_argument(&mut self, dest: &str, nargs: i32) {
self.args.push(ParserArgument {
name: dest.to_string(),
nargs,
dest: dest.to_string(),
});
}
pub fn parse_args(&self, args: Vec<String>) -> ParseResult {
let mut state = ParsingState::new(args);
self.process_args_for_options(&mut state)?;
self.process_args_for_args(&mut state)?;
Ok((state.opts, state.largs, state.order))
}
fn process_args_for_options(&self, state: &mut ParsingState) -> Result<(), ClickError> {
while let Some(arg) = state.rargs.pop_front() {
let arglen = arg.len();
if arg == "--" {
return Ok(());
}
let first_char = arg.chars().next().unwrap_or(' ');
let is_option_like = self.opt_prefixes.contains(&first_char.to_string()) && arglen > 1;
if is_option_like {
self.process_opts(&arg, state)?;
} else if self.allow_interspersed_args {
state.largs.push(arg);
} else {
state.rargs.push_front(arg);
return Ok(());
}
}
Ok(())
}
fn process_args_for_args(&self, state: &mut ParsingState) -> Result<(), ClickError> {
let all_args: Vec<String> = state
.largs
.drain(..)
.chain(state.rargs.drain(..))
.collect();
let nargs_spec: Vec<i32> = self.args.iter().map(|a| a.nargs).collect();
let (parsed, remaining) = unpack_args(&all_args, &nargs_spec)?;
for (idx, arg_def) in self.args.iter().enumerate() {
if idx < parsed.len() {
let value = &parsed[idx];
if arg_def.nargs > 1 {
if let ParsedValue::Multiple(ref values) = value {
if values.len() < arg_def.nargs as usize {
return Err(ClickError::bad_argument_usage(
format!(
"Argument '{}' takes {} values.",
arg_def.dest, arg_def.nargs
),
));
}
}
}
state.opts.insert(arg_def.dest.clone(), value.clone());
if !value.is_unset() {
state.order.push(arg_def.dest.clone());
}
} else {
state.opts.insert(arg_def.dest.clone(), ParsedValue::Unset);
}
}
state.largs = remaining;
Ok(())
}
fn process_opts(&self, arg: &str, state: &mut ParsingState) -> Result<(), ClickError> {
let mut explicit_value = None;
let long_opt;
if let Some(eq_pos) = arg.find('=') {
long_opt = arg[..eq_pos].to_string();
explicit_value = Some(arg[eq_pos + 1..].to_string());
} else {
long_opt = arg.to_string();
}
let norm_long_opt = self.normalize_opt(&long_opt);
match self.match_long_opt(&norm_long_opt, explicit_value.clone(), state) {
Ok(()) => Ok(()),
Err(ClickError::NoSuchOption { .. }) => {
if let Some((prefix, _)) = split_opt(arg) {
if prefix.len() == 1 {
return self.match_short_opt(arg, state);
}
}
if self.ignore_unknown_options {
state.largs.push(arg.to_string());
return Ok(());
}
self.match_long_opt(&norm_long_opt, explicit_value, state)
}
Err(e) => Err(e),
}
}
fn match_long_opt(
&self,
opt: &str,
explicit_value: Option<String>,
state: &mut ParsingState,
) -> Result<(), ClickError> {
let option = match self.long_opt.get(opt) {
Some(o) => o.clone(),
None => {
let all_opts: Vec<&str> = self.long_opt.keys().map(|s| s.as_str()).collect();
let possibilities = get_close_matches(opt, &all_opts, 3);
return Err(if possibilities.is_empty() {
ClickError::no_such_option(opt)
} else {
ClickError::no_such_option_with_suggestions(opt, possibilities)
});
}
};
if option.takes_value() {
if let Some(val) = explicit_value {
state.rargs.push_front(val);
}
let value = self.get_value_from_state(opt, &option, state)?;
self.process_option(&option, value, state);
} else if explicit_value.is_some() {
return Err(ClickError::bad_option_usage(
opt,
format!("Option '{}' does not take a value.", opt),
));
} else {
self.process_option(&option, ParsedValue::Unset, state);
}
Ok(())
}
fn match_short_opt(&self, arg: &str, state: &mut ParsingState) -> Result<(), ClickError> {
let prefix = arg.chars().next().unwrap();
let mut i = 1;
let chars: Vec<char> = arg.chars().collect();
let mut unknown_options = Vec::new();
while i < chars.len() {
let ch = chars[i];
let opt = self.normalize_opt(&format!("{}{}", prefix, ch));
i += 1;
let option = match self.short_opt.get(&opt) {
Some(o) => o.clone(),
None => {
if self.ignore_unknown_options {
unknown_options.push(ch);
continue;
}
return Err(ClickError::no_such_option(&opt));
}
};
if option.takes_value() {
if i < chars.len() {
let value: String = chars[i..].iter().collect();
state.rargs.push_front(value);
}
let value = self.get_value_from_state(&opt, &option, state)?;
self.process_option(&option, value, state);
break;
} else {
self.process_option(&option, ParsedValue::Unset, state);
}
}
if self.ignore_unknown_options && !unknown_options.is_empty() {
let combined: String =
std::iter::once(prefix).chain(unknown_options).collect();
state.largs.push(combined);
}
Ok(())
}
fn get_value_from_state(
&self,
option_name: &str,
option: &ParserOption,
state: &mut ParsingState,
) -> Result<ParsedValue, ClickError> {
let nargs = option.nargs;
if nargs == NARGS_OPTIONAL {
if let Some(next_arg) = state.rargs.front() {
let first_char = next_arg.chars().next().unwrap_or(' ');
let looks_like_option = self.opt_prefixes.contains(&first_char.to_string())
&& next_arg.len() > 1;
if looks_like_option && option.flag_needs_value {
return Ok(ParsedValue::FlagNeedsValue);
}
} else if option.flag_needs_value {
return Ok(ParsedValue::FlagNeedsValue);
}
if let Some(value) = state.rargs.pop_front() {
return Ok(ParsedValue::Single(value));
} else {
return Ok(ParsedValue::Unset);
}
}
if nargs <= 0 {
return Ok(ParsedValue::Unset);
}
let rargs_len = state.rargs.len() as i32;
if rargs_len < nargs {
if option.flag_needs_value {
return Ok(ParsedValue::FlagNeedsValue);
}
return Err(ClickError::bad_option_usage(
option_name,
if nargs == 1 {
format!("Option '{}' requires an argument.", option_name)
} else {
format!("Option '{}' requires {} arguments.", option_name, nargs)
},
));
}
if nargs == 1 {
if option.flag_needs_value {
if let Some(next_arg) = state.rargs.front() {
let first_char = next_arg.chars().next().unwrap_or(' ');
let looks_like_option = self.opt_prefixes.contains(&first_char.to_string())
&& next_arg.len() > 1;
if looks_like_option {
return Ok(ParsedValue::FlagNeedsValue);
}
}
}
let value = state.rargs.pop_front().unwrap();
Ok(ParsedValue::Single(value))
} else {
let values: Vec<String> = (0..nargs)
.filter_map(|_| state.rargs.pop_front())
.collect();
Ok(ParsedValue::Multiple(values))
}
}
fn process_option(&self, option: &ParserOption, value: ParsedValue, state: &mut ParsingState) {
match option.action {
OptionAction::Store => {
state.opts.insert(option.dest.clone(), value);
}
OptionAction::StoreConst => {
let const_val = option
.const_value
.clone()
.map(ParsedValue::Single)
.unwrap_or(ParsedValue::Flag(true));
state.opts.insert(option.dest.clone(), const_val);
}
OptionAction::Append => {
let entry = state
.opts
.entry(option.dest.clone())
.or_insert_with(|| ParsedValue::Multiple(Vec::new()));
if let ParsedValue::Multiple(ref mut vec) = entry {
match value {
ParsedValue::Single(s) => vec.push(s),
ParsedValue::Multiple(v) => vec.extend(v),
ParsedValue::FlagNeedsValue => {
state.opts.insert(
format!("__click_internal_flag_needs_value_{}", option.dest),
ParsedValue::Flag(true),
);
}
_ => {}
}
}
}
OptionAction::AppendConst => {
let entry = state
.opts
.entry(option.dest.clone())
.or_insert_with(|| ParsedValue::Multiple(Vec::new()));
if let ParsedValue::Multiple(ref mut vec) = entry {
if let Some(ref const_val) = option.const_value {
vec.push(const_val.clone());
}
}
}
OptionAction::Count => {
let entry = state
.opts
.entry(option.dest.clone())
.or_insert(ParsedValue::Count(0));
if let ParsedValue::Count(ref mut n) = entry {
*n += 1;
}
}
}
state.order.push(option.dest.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_opt_long() {
assert_eq!(split_opt("--name"), Some(("--", "name")));
assert_eq!(split_opt("--full-name"), Some(("--", "full-name")));
}
#[test]
fn test_split_opt_short() {
assert_eq!(split_opt("-n"), Some(("-", "n")));
assert_eq!(split_opt("-abc"), Some(("-", "abc")));
}
#[test]
fn test_split_opt_no_prefix() {
assert_eq!(split_opt("name"), None);
assert_eq!(split_opt(""), None);
}
#[test]
fn test_split_opt_just_dashes() {
assert_eq!(split_opt("-"), None);
assert_eq!(split_opt("--"), None);
}
#[test]
fn test_edit_distance() {
assert_eq!(edit_distance("", ""), 0);
assert_eq!(edit_distance("a", ""), 1);
assert_eq!(edit_distance("", "b"), 1);
assert_eq!(edit_distance("abc", "abc"), 0);
assert_eq!(edit_distance("abc", "abd"), 1);
assert_eq!(edit_distance("help", "hlep"), 2);
assert_eq!(edit_distance("kitten", "sitting"), 3);
}
#[test]
fn test_get_close_matches() {
let opts = vec!["--help", "--hello", "--version", "--verbose"];
let matches = get_close_matches("--hlep", &opts, 3);
assert!(matches.contains(&"--help".to_string()));
}
#[test]
fn test_short_option_with_space() {
let mut parser = OptionParser::new();
parser.add_option(&["-n"], "name", OptionAction::Store, 1, None);
let args = vec!["-n".to_string(), "value".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("value".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_short_option_with_equals() {
let mut parser = OptionParser::new();
parser.add_option(&["-n"], "name", OptionAction::Store, 1, None);
let args = vec!["-n=value".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("=value".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_short_option_attached_value() {
let mut parser = OptionParser::new();
parser.add_option(&["-n"], "name", OptionAction::Store, 1, None);
let args = vec!["-nvalue".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("value".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_grouped_short_flags() {
let mut parser = OptionParser::new();
parser.add_option(&["-a"], "a", OptionAction::StoreConst, 0, Some("true"));
parser.add_option(&["-b"], "b", OptionAction::StoreConst, 0, Some("true"));
parser.add_option(&["-c"], "c", OptionAction::StoreConst, 0, Some("true"));
let args = vec!["-abc".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("a"),
Some(&ParsedValue::Single("true".to_string()))
);
assert_eq!(
opts.get("b"),
Some(&ParsedValue::Single("true".to_string()))
);
assert_eq!(
opts.get("c"),
Some(&ParsedValue::Single("true".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_grouped_short_with_value() {
let mut parser = OptionParser::new();
parser.add_option(&["-a"], "a", OptionAction::StoreConst, 0, Some("true"));
parser.add_option(&["-b"], "b", OptionAction::StoreConst, 0, Some("true"));
parser.add_option(&["-n"], "name", OptionAction::Store, 1, None);
let args = vec!["-abn".to_string(), "value".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("a"),
Some(&ParsedValue::Single("true".to_string()))
);
assert_eq!(
opts.get("b"),
Some(&ParsedValue::Single("true".to_string()))
);
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("value".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_grouped_short_with_attached_value() {
let mut parser = OptionParser::new();
parser.add_option(&["-a"], "a", OptionAction::StoreConst, 0, Some("true"));
parser.add_option(&["-n"], "name", OptionAction::Store, 1, None);
let args = vec!["-anVALUE".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("a"),
Some(&ParsedValue::Single("true".to_string()))
);
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("VALUE".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_long_option_with_space() {
let mut parser = OptionParser::new();
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
let args = vec!["--name".to_string(), "value".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("value".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_long_option_with_equals() {
let mut parser = OptionParser::new();
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
let args = vec!["--name=value".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("value".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_long_option_multiple_args() {
let mut parser = OptionParser::new();
parser.add_option(&["--point"], "point", OptionAction::Store, 2, None);
let args = vec!["--point".to_string(), "1".to_string(), "2".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("point"),
Some(&ParsedValue::Multiple(vec!["1".to_string(), "2".to_string()]))
);
assert!(remaining.is_empty());
}
#[test]
fn test_double_dash_terminator() {
let mut parser = OptionParser::new();
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
parser.add_argument("files", -1);
let args = vec![
"--name".to_string(),
"test".to_string(),
"--".to_string(),
"--not-an-option".to_string(),
"file.txt".to_string(),
];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("test".to_string()))
);
assert_eq!(
opts.get("files"),
Some(&ParsedValue::Multiple(vec![
"--not-an-option".to_string(),
"file.txt".to_string()
]))
);
assert!(remaining.is_empty());
}
#[test]
fn test_interspersed_args_allowed() {
let mut parser = OptionParser::new().allow_interspersed_args(true);
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
parser.add_argument("files", -1);
let args = vec![
"file1.txt".to_string(),
"--name".to_string(),
"test".to_string(),
"file2.txt".to_string(),
];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("test".to_string()))
);
assert_eq!(
opts.get("files"),
Some(&ParsedValue::Multiple(vec![
"file1.txt".to_string(),
"file2.txt".to_string()
]))
);
assert!(remaining.is_empty());
}
#[test]
fn test_interspersed_args_not_allowed() {
let mut parser = OptionParser::new().allow_interspersed_args(false);
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
parser.add_argument("files", -1);
let args = vec![
"file1.txt".to_string(),
"--name".to_string(),
"test".to_string(),
"file2.txt".to_string(),
];
let (opts, _remaining, _) = parser.parse_args(args).unwrap();
assert!(opts.get("name").is_none() || opts.get("name") == Some(&ParsedValue::Unset));
assert_eq!(
opts.get("files"),
Some(&ParsedValue::Multiple(vec![
"file1.txt".to_string(),
"--name".to_string(),
"test".to_string(),
"file2.txt".to_string()
]))
);
}
#[test]
fn test_unknown_option_error() {
let parser = OptionParser::new();
let args = vec!["--unknown".to_string()];
let result = parser.parse_args(args);
assert!(result.is_err());
match result.unwrap_err() {
ClickError::NoSuchOption { option_name, .. } => {
assert_eq!(option_name, "--unknown");
}
_ => panic!("Expected NoSuchOption error"),
}
}
#[test]
fn test_unknown_option_with_suggestion() {
let mut parser = OptionParser::new();
parser.add_option(&["--help"], "help", OptionAction::StoreConst, 0, Some("true"));
let args = vec!["--hlep".to_string()];
let result = parser.parse_args(args);
assert!(result.is_err());
match result.unwrap_err() {
ClickError::NoSuchOption {
option_name,
possibilities,
..
} => {
assert_eq!(option_name, "--hlep");
assert!(possibilities.is_some());
assert!(possibilities.unwrap().contains(&"--help".to_string()));
}
_ => panic!("Expected NoSuchOption error with suggestions"),
}
}
#[test]
fn test_ignore_unknown_options() {
let mut parser = OptionParser::new().ignore_unknown_options(true);
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
parser.add_argument("files", -1);
let args = vec![
"--unknown".to_string(),
"--name".to_string(),
"test".to_string(),
];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("test".to_string()))
);
if let Some(ParsedValue::Multiple(files)) = opts.get("files") {
assert!(files.contains(&"--unknown".to_string()));
}
}
#[test]
fn test_count_action() {
let mut parser = OptionParser::new();
parser.add_option(&["-v", "--verbose"], "verbose", OptionAction::Count, 0, None);
let args = vec![
"-v".to_string(),
"-v".to_string(),
"--verbose".to_string(),
];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(opts.get("verbose"), Some(&ParsedValue::Count(3)));
}
#[test]
fn test_append_action() {
let mut parser = OptionParser::new();
parser.add_option(&["-f", "--file"], "files", OptionAction::Append, 1, None);
let args = vec![
"-f".to_string(),
"a.txt".to_string(),
"--file".to_string(),
"b.txt".to_string(),
"-f".to_string(),
"c.txt".to_string(),
];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("files"),
Some(&ParsedValue::Multiple(vec![
"a.txt".to_string(),
"b.txt".to_string(),
"c.txt".to_string()
]))
);
}
#[test]
fn test_append_const_action() {
let mut parser = OptionParser::new();
parser.add_option(
&["--debug"],
"flags",
OptionAction::AppendConst,
0,
Some("debug"),
);
parser.add_option(
&["--trace"],
"flags",
OptionAction::AppendConst,
0,
Some("trace"),
);
let args = vec!["--debug".to_string(), "--trace".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("flags"),
Some(&ParsedValue::Multiple(vec![
"debug".to_string(),
"trace".to_string()
]))
);
}
#[test]
fn test_store_const_action() {
let mut parser = OptionParser::new();
parser.add_option(
&["--debug"],
"debug",
OptionAction::StoreConst,
0,
Some("true"),
);
let args = vec!["--debug".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("debug"),
Some(&ParsedValue::Single("true".to_string()))
);
}
#[test]
fn test_single_positional_argument() {
let mut parser = OptionParser::new();
parser.add_argument("file", 1);
let args = vec!["input.txt".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("file"),
Some(&ParsedValue::Single("input.txt".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_variadic_positional_argument() {
let mut parser = OptionParser::new();
parser.add_argument("files", -1);
let args = vec!["a.txt".to_string(), "b.txt".to_string(), "c.txt".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("files"),
Some(&ParsedValue::Multiple(vec![
"a.txt".to_string(),
"b.txt".to_string(),
"c.txt".to_string()
]))
);
assert!(remaining.is_empty());
}
#[test]
fn test_multiple_positional_arguments() {
let mut parser = OptionParser::new();
parser.add_argument("source", 1);
parser.add_argument("dest", 1);
let args = vec!["input.txt".to_string(), "output.txt".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("source"),
Some(&ParsedValue::Single("input.txt".to_string()))
);
assert_eq!(
opts.get("dest"),
Some(&ParsedValue::Single("output.txt".to_string()))
);
assert!(remaining.is_empty());
}
#[test]
fn test_positional_with_variadic() {
let mut parser = OptionParser::new();
parser.add_argument("dest", 1);
parser.add_argument("sources", -1);
let args = vec!["out.txt".to_string(), "a.txt".to_string(), "b.txt".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("dest"),
Some(&ParsedValue::Single("out.txt".to_string()))
);
assert_eq!(
opts.get("sources"),
Some(&ParsedValue::Multiple(vec![
"a.txt".to_string(),
"b.txt".to_string()
]))
);
assert!(remaining.is_empty());
}
#[test]
fn test_missing_required_positional() {
let mut parser = OptionParser::new();
parser.add_argument("file", 1);
let args: Vec<String> = vec![];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(opts.get("file"), Some(&ParsedValue::Unset));
}
#[test]
fn test_options_and_arguments() {
let mut parser = OptionParser::new();
parser.add_option(&["-n", "--name"], "name", OptionAction::Store, 1, None);
parser.add_option(&["-v", "--verbose"], "verbose", OptionAction::Count, 0, None);
parser.add_argument("file", 1);
let args = vec![
"-v".to_string(),
"--name".to_string(),
"test".to_string(),
"-v".to_string(),
"input.txt".to_string(),
];
let (opts, remaining, order) = parser.parse_args(args).unwrap();
assert_eq!(opts.get("verbose"), Some(&ParsedValue::Count(2)));
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("test".to_string()))
);
assert_eq!(
opts.get("file"),
Some(&ParsedValue::Single("input.txt".to_string()))
);
assert!(remaining.is_empty());
assert!(order.contains(&"verbose".to_string()));
assert!(order.contains(&"name".to_string()));
assert!(order.contains(&"file".to_string()));
}
#[test]
fn test_missing_option_value() {
let mut parser = OptionParser::new();
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
let args = vec!["--name".to_string()];
let result = parser.parse_args(args);
assert!(result.is_err());
match result.unwrap_err() {
ClickError::BadOptionUsage { option_name, .. } => {
assert_eq!(option_name, "--name");
}
_ => panic!("Expected BadOptionUsage error"),
}
}
#[test]
fn test_option_takes_no_value() {
let mut parser = OptionParser::new();
parser.add_option(
&["--debug"],
"debug",
OptionAction::StoreConst,
0,
Some("true"),
);
let args = vec!["--debug=value".to_string()];
let result = parser.parse_args(args);
assert!(result.is_err());
match result.unwrap_err() {
ClickError::BadOptionUsage { option_name, .. } => {
assert_eq!(option_name, "--debug");
}
_ => panic!("Expected BadOptionUsage error"),
}
}
#[test]
fn test_token_normalize_func() {
let mut parser =
OptionParser::new().token_normalize_func(|s| s.to_lowercase());
parser.add_option(&["--name"], "name", OptionAction::Store, 1, None);
let args = vec!["--NAME".to_string(), "value".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("name"),
Some(&ParsedValue::Single("value".to_string()))
);
}
#[test]
fn test_parsed_value_accessors() {
let single = ParsedValue::Single("test".to_string());
assert_eq!(single.as_single(), Some("test"));
assert!(single.as_multiple().is_none());
assert!(single.as_count().is_none());
assert!(single.as_flag().is_none());
assert!(!single.is_unset());
let multiple = ParsedValue::Multiple(vec!["a".to_string(), "b".to_string()]);
assert!(multiple.as_single().is_none());
assert_eq!(
multiple.as_multiple(),
Some(&["a".to_string(), "b".to_string()][..])
);
let count = ParsedValue::Count(5);
assert_eq!(count.as_count(), Some(5));
let flag = ParsedValue::Flag(true);
assert_eq!(flag.as_flag(), Some(true));
let unset = ParsedValue::Unset;
assert!(unset.is_unset());
}
#[test]
fn test_unpack_args_simple() {
let args = vec!["a".to_string(), "b".to_string()];
let specs = vec![1, 1];
let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Single("a".to_string()));
assert_eq!(result[1], ParsedValue::Single("b".to_string()));
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_variadic() {
let args = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let specs = vec![-1]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(
result[0],
ParsedValue::Multiple(vec![
"a".to_string(),
"b".to_string(),
"c".to_string()
])
);
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_with_variadic_in_middle() {
let args = vec![
"dest".to_string(),
"a".to_string(),
"b".to_string(),
"c".to_string(),
];
let specs = vec![1, -1]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Single("dest".to_string()));
assert_eq!(
result[1],
ParsedValue::Multiple(vec![
"a".to_string(),
"b".to_string(),
"c".to_string()
])
);
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_missing() {
let args = vec!["a".to_string()];
let specs = vec![1, 1];
let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Single("a".to_string()));
assert_eq!(result[1], ParsedValue::Unset);
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_empty_variadic() {
let args: Vec<String> = vec![];
let specs = vec![-1];
let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ParsedValue::Multiple(vec![]));
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_optional_with_value() {
let args = vec!["value".to_string()];
let specs = vec![NARGS_OPTIONAL]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ParsedValue::Single("value".to_string()));
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_optional_without_value() {
let args: Vec<String> = vec![];
let specs = vec![NARGS_OPTIONAL]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ParsedValue::Unset);
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_optional_after_required() {
let args = vec!["required".to_string(), "optional".to_string()];
let specs = vec![1, NARGS_OPTIONAL]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Single("required".to_string()));
assert_eq!(result[1], ParsedValue::Single("optional".to_string()));
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_optional_missing_after_required() {
let args = vec!["required".to_string()];
let specs = vec![1, NARGS_OPTIONAL]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Single("required".to_string()));
assert_eq!(result[1], ParsedValue::Unset);
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_optional_before_required_preserves_required() {
let args = vec!["x".to_string()];
let specs = vec![NARGS_OPTIONAL, 1]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Unset);
assert_eq!(result[1], ParsedValue::Single("x".to_string()));
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_optional_before_required_with_both() {
let args = vec!["opt".to_string(), "req".to_string()];
let specs = vec![NARGS_OPTIONAL, 1]; let (result, remaining) = unpack_args(&args, &specs).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ParsedValue::Single("opt".to_string()));
assert_eq!(result[1], ParsedValue::Single("req".to_string()));
assert!(remaining.is_empty());
}
#[test]
fn test_unpack_args_two_variadic_error() {
let args = vec!["a".to_string(), "b".to_string()];
let specs = vec![-1, -1]; let result = unpack_args(&args, &specs);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("variadic"));
}
#[test]
fn test_option_with_optional_value_explicit() {
let mut parser = OptionParser::new();
parser.add_option_ex(
&["--opt"],
"opt",
OptionAction::Store,
1,
None,
true, );
let args = vec!["--opt=value".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("opt"),
Some(&ParsedValue::Single("value".to_string()))
);
}
#[test]
fn test_option_with_optional_value_followed_by_option() {
let mut parser = OptionParser::new();
parser.add_option_ex(
&["--opt"],
"opt",
OptionAction::Store,
1,
None,
true, );
parser.add_option(&["--other"], "other", OptionAction::StoreConst, 0, Some("true"));
let args = vec!["--opt".to_string(), "--other".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(opts.get("opt"), Some(&ParsedValue::FlagNeedsValue));
assert_eq!(
opts.get("other"),
Some(&ParsedValue::Single("true".to_string()))
);
}
#[test]
fn test_option_with_optional_value_with_value() {
let mut parser = OptionParser::new();
parser.add_option_ex(
&["--opt"],
"opt",
OptionAction::Store,
1,
None,
true, );
let args = vec!["--opt".to_string(), "value".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("opt"),
Some(&ParsedValue::Single("value".to_string()))
);
}
#[test]
fn test_option_with_optional_value_at_end() {
let mut parser = OptionParser::new();
parser.add_option_ex(
&["--opt"],
"opt",
OptionAction::Store,
1,
None,
true, );
let args = vec!["--opt".to_string()];
let (opts, _, _) = parser.parse_args(args).unwrap();
assert_eq!(opts.get("opt"), Some(&ParsedValue::FlagNeedsValue));
}
#[test]
fn test_multi_value_argument_incomplete() {
let mut parser = OptionParser::new();
parser.add_argument("pair", 2);
let args = vec!["first".to_string()];
let result = parser.parse_args(args);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("takes 2 values"));
}
#[test]
fn test_multi_value_argument_complete() {
let mut parser = OptionParser::new();
parser.add_argument("pair", 2);
let args = vec!["first".to_string(), "second".to_string()];
let (opts, remaining, _) = parser.parse_args(args).unwrap();
assert_eq!(
opts.get("pair"),
Some(&ParsedValue::Multiple(vec![
"first".to_string(),
"second".to_string()
]))
);
assert!(remaining.is_empty());
}
}