From 2465588d7fa9bebd6c505d75e2bd4e8cdfc20fb2 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 10 Nov 2023 08:04:02 +0800 Subject: [PATCH 1/4] feat: support option starts with `+` --- src/command/mod.rs | 28 ++++-------- src/param.rs | 53 ++++++++++++---------- src/parser.rs | 109 +++++++++++++++++++++++++++++---------------- 3 files changed, 110 insertions(+), 80 deletions(-) diff --git a/src/command/mod.rs b/src/command/mod.rs index e2cc0a00..2a639443 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -361,10 +361,10 @@ impl Command { } let mut list = vec![]; let mut any_describe = false; - let mut single_hyphen = false; + let mut single = false; for param in self.flag_option_params.iter() { - if param.single_hyphen { - single_hyphen = true; + if param.single { + single = true; } let value = param.render_body(); let describe = param.render_describe(); @@ -373,8 +373,8 @@ impl Command { } list.push((value, describe)); } - self.add_help_flag(&mut list, single_hyphen, any_describe); - self.add_version_flag(&mut list, single_hyphen, any_describe); + self.add_help_flag(&mut list, single, any_describe); + self.add_version_flag(&mut list, single, any_describe); output.push("OPTIONS:".to_string()); let value_size = list.iter().map(|v| v.0.len()).max().unwrap_or_default() + 2; for (value, describe) in list { @@ -593,16 +593,11 @@ impl Command { self.subcommands.last_mut().unwrap() } - fn add_help_flag( - &self, - list: &mut Vec<(String, String)>, - single_hyphen: bool, - any_describe: bool, - ) { + fn add_help_flag(&self, list: &mut Vec<(String, String)>, single: bool, any_describe: bool) { if self.find_flag_option("help").is_some() { return; } - let hyphens = if single_hyphen { " -" } else { "--" }; + let hyphens = if single { " -" } else { "--" }; list.push(( if self.match_help_short_name() { format!("-h, {}help", hyphens) @@ -617,19 +612,14 @@ impl Command { )); } - fn add_version_flag( - &self, - list: &mut Vec<(String, String)>, - single_hyphen: bool, - any_describe: bool, - ) { + fn add_version_flag(&self, list: &mut Vec<(String, String)>, single: bool, any_describe: bool) { if self.version.is_none() { return; } if self.find_flag_option("version").is_some() { return; } - let hyphens = if single_hyphen { " -" } else { "--" }; + let hyphens = if single { " -" } else { "--" }; list.push(( if self.match_version_short_name() { format!("-V, {}version", hyphens) diff --git a/src/param.rs b/src/param.rs index 79ed98c5..1abe7188 100644 --- a/src/param.rs +++ b/src/param.rs @@ -11,7 +11,8 @@ pub(crate) struct FlagOptionParam { pub(crate) describe: String, pub(crate) short: Option, pub(crate) flag: bool, - pub(crate) single_hyphen: bool, + pub(crate) sign: char, + pub(crate) single: bool, pub(crate) data: ParamData, pub(crate) value_names: Vec, pub(crate) arg_value_names: Vec, @@ -24,7 +25,8 @@ impl FlagOptionParam { describe: &str, short: Option, flag: bool, - single_hyphen: bool, + sign: char, + single: bool, value_names: &[&str], ) -> Self { let name = param.name.clone(); @@ -44,7 +46,8 @@ impl FlagOptionParam { describe: describe.to_string(), short, flag, - single_hyphen, + sign, + single, data: param, value_names, arg_value_names, @@ -137,11 +140,11 @@ impl FlagOptionParam { pub(crate) fn render_source(&self) -> String { let mut output = vec![]; if let Some(ch) = self.short { - output.push(format!("-{}", ch)); + output.push(format!("{}{}", self.sign, ch)); }; output.push(format!( "{}{}", - self.render_hyphens(), + self.render_long_prefix(), self.data.render_name_value() )); for value_name in &self.value_names { @@ -153,16 +156,20 @@ impl FlagOptionParam { output.join(" ") } - pub(crate) fn render_hyphens(&self) -> &str { - if self.single_hyphen { - "-" + pub(crate) fn render_long_prefix(&self) -> &str { + if self.single { + if self.sign == '+' { + "+" + } else { + "-" + } } else { "--" } } pub(crate) fn render_name(&self) -> String { - format!("{}{}", self.render_hyphens(), self.name()) + format!("{}{}", self.render_long_prefix(), self.name()) } pub(crate) fn render_first_notation(&self) -> String { @@ -180,19 +187,16 @@ impl FlagOptionParam { pub(crate) fn render_body(&self) -> String { let mut output = String::new(); - if self.single_hyphen && self.short.is_none() && self.name().len() == 1 { - output.push_str(&format!("-{}", self.name())); + let sign = self.sign; + if self.single && self.short.is_none() && self.name().len() == 1 { + output.push_str(&format!("{sign}{}", self.name())); } else { if let Some(ch) = self.short { - output.push_str(&format!("-{ch}, ")) + output.push_str(&format!("{sign}{ch}, ")) } else { output.push_str(" ") }; - if self.single_hyphen { - output.push_str(" -") - } else { - output.push_str("--") - } + output.push_str(&format!("{:>2}", self.render_long_prefix())); output.push_str(self.name()); } @@ -236,7 +240,10 @@ impl FlagOptionParam { } pub(crate) fn get_arg_value(&self, values: &[&[&str]]) -> Option { - let name = self.name().to_string(); + let mut name = self.name().to_string(); + if self.sign == '+' { + name = format!("plus_{name}") + } if self.flag { if values.is_empty() { None @@ -296,7 +303,7 @@ impl FlagOptionParam { } if let Some(ch) = self.short { - return Some(format!("-{ch}")); + return Some(format!("{}{ch}", self.sign)); } Some(self.render_name()) @@ -304,9 +311,9 @@ impl FlagOptionParam { pub(crate) fn list_names(&self) -> Vec { let mut output = vec![]; - output.push(format!("{}{}", self.render_hyphens(), self.name())); + output.push(format!("{}{}", self.render_long_prefix(), self.name())); if let Some(short) = self.short { - output.push(format!("-{}", short)); + output.push(format!("{}{}", self.sign, short)); } output } @@ -540,8 +547,8 @@ impl ParamData { result.push_str(&format!("[={}]", Self::render_choice_values(values))); } (Some(ChoiceData::Fn(f, validate)), _) => { - let sign = if *validate { "" } else { "?" }; - result.push_str(&format!("[{sign}`{f}`]")); + let prefix = if *validate { "" } else { "?" }; + result.push_str(&format!("[{prefix}`{f}`]")); } (None, Some(DefaultData::Value(value))) => { result.push_str(&format!("={}", Self::render_default_value(value))); diff --git a/src/parser.rs b/src/parser.rs index 285d82a0..6741c5f2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,6 +4,7 @@ use crate::param::{ use crate::utils::{is_choice_value_terminate, is_default_value_terminate}; use crate::Result; use anyhow::bail; +use nom::bytes::complete::is_not; use nom::error::ErrorKind; use nom::{ branch::alt, @@ -222,8 +223,7 @@ fn parse_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( tuple(( - parse_short, - preceded(space0, alt((tag("--"), tag("-")))), + parse_with_long_head, alt(( parse_param_modifer_choices_default, parse_param_modifer_choices_fn, @@ -235,15 +235,8 @@ fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionPar parse_zero_or_many_value_notations, parse_tail, )), - |(short, hyphens, arg, value_names, describe)| { - FlagOptionParam::new( - arg, - describe, - short, - false, - hyphens.len() == 1, - &value_names, - ) + |((sign, short, single), arg, value_names, describe)| { + FlagOptionParam::new(arg, describe, short, false, sign, single, &value_names) }, )(input) } @@ -252,25 +245,23 @@ fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionPar fn parse_no_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( tuple(( + preceded(space0, alt((char('-'), char('+')))), preceded( - pair(space0, tag("-")), - preceded( - verify_single_char, - alt(( - parse_param_modifer_choices_default, - parse_param_modifer_choices_fn, - parse_param_modifer_choices, - parse_param_assign_fn, - parse_param_assign, - parse_param_modifer, - )), - ), + verify_single_char, + alt(( + parse_param_modifer_choices_default, + parse_param_modifer_choices_fn, + parse_param_modifer_choices, + parse_param_assign_fn, + parse_param_assign, + parse_param_modifer, + )), ), parse_zero_or_many_value_notations, parse_tail, )), - |(arg, value_names, describe)| { - FlagOptionParam::new(arg, describe, None, false, true, &value_names) + |(sign, arg, value_names, describe)| { + FlagOptionParam::new(arg, describe, None, false, sign, true, &value_names) }, )(input) } @@ -303,13 +294,12 @@ fn parse_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { fn parse_with_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( tuple(( - parse_short, - preceded(space0, alt((tag("--"), tag("-")))), + parse_with_long_head, parse_long_flag_and_asterisk, parse_tail, )), - |(short, hyphens, arg, describe)| { - FlagOptionParam::new(arg, describe, short, true, hyphens.len() == 1, &[]) + |((sign, short, single), arg, describe)| { + FlagOptionParam::new(arg, describe, short, true, sign, single, &[]) }, )(input) } @@ -318,10 +308,11 @@ fn parse_with_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam fn parse_no_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( tuple(( - preceded(pair(space0, tag("-")), parse_short_flag_and_asterisk), + preceded(space0, alt((char('-'), char('+')))), + parse_short_flag_and_asterisk, parse_tail, )), - |(arg, describe)| FlagOptionParam::new(arg, describe, None, true, true, &[]), + |(sign, arg, describe)| FlagOptionParam::new(arg, describe, None, true, sign, true, &[]), )(input) } @@ -336,7 +327,7 @@ fn parse_long_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { ))(input) } -// Parse ':' or '#' or '0' +// Parse 'ch*' or 'ch` fn parse_short_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { fn parser(input: &str) -> nom::IResult<&str, ParamData> { map(satisfy(is_short_char), |ch| { @@ -351,6 +342,31 @@ fn parse_short_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { })(input) } +fn parse_with_long_head(input: &str) -> nom::IResult<&str, (char, Option, bool)> { + map( + alt(( + pair( + opt(terminated( + pair(char::<&str, _>('-'), satisfy(is_short_char)), + peek(space1), + )), + preceded(space0, alt((tag("--"), tag("-")))), + ), + pair( + opt(terminated( + pair(char::<&str, _>('+'), satisfy(is_short_char)), + peek(space1), + )), + terminated(preceded(space0, tag("+")), peek(is_not("+"))), + ), + )), + |(short, long)| match short { + Some((sign, short)) => (sign, Some(short), long.len() == 1), + None => (long.chars().next().unwrap(), None, long.len() == 1), + }, + )(input) +} + // Parse `str!` `str~` `str*` `str+` `str` fn parse_param_modifer(input: &str) -> nom::IResult<&str, ParamData> { alt(( @@ -473,12 +489,6 @@ fn parse_param_name(input: &str) -> nom::IResult<&str, ParamData> { map(parse_name, ParamData::new)(input) } -// Parse `-s` -fn parse_short(input: &str) -> nom::IResult<&str, Option> { - let short = delimited(char('-'), satisfy(is_short_char), peek(space1)); - opt(short)(input) -} - // Zero or many '' fn parse_zero_or_many_value_notations(input: &str) -> nom::IResult<&str, Vec<&str>> { many0(parse_value_notation)(input) @@ -926,6 +936,29 @@ mod tests { assert_parse_flag_arg!("-f*"); } + #[test] + fn test_parse_with_long_head() { + assert_eq!( + parse_with_long_head("-f --foo"), + Ok(("foo", ('-', Some('f'), false))) + ); + assert_eq!( + parse_with_long_head("-f -foo"), + Ok(("foo", ('-', Some('f'), true))) + ); + assert_eq!( + parse_with_long_head("--foo"), + Ok(("foo", ('-', None, false))) + ); + assert_eq!(parse_with_long_head("-foo"), Ok(("foo", ('-', None, true)))); + assert_eq!( + parse_with_long_head("+f +foo"), + Ok(("foo", ('+', Some('f'), true))) + ); + assert_eq!(parse_with_long_head("+foo"), Ok(("foo", ('+', None, true)))); + assert!(parse_with_long_head("++foo").is_err()); + } + #[test] fn test_parse_positional_arg() { assert_parse_positional_arg!("foo A foo arg"); From 65f415716fa1dc199b2eb59b740dc8bc4d2028e8 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 10 Nov 2023 08:22:59 +0800 Subject: [PATCH 2/4] use var_name --- src/command/mod.rs | 8 +++--- src/command/names_checker.rs | 4 +-- src/matcher.rs | 46 +++++++++++++++-------------- src/param.rs | 56 +++++++++++++++++++----------------- 4 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/command/mod.rs b/src/command/mod.rs index 2a639443..b911b4ac 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -471,7 +471,7 @@ impl Command { pub(crate) fn find_flag_option(&self, name: &str) -> Option<&FlagOptionParam> { self.flag_option_params .iter() - .find(|v| v.name() == name || v.is_match(name)) + .find(|v| v.var_name() == name || v.is_match(name)) } pub(crate) fn find_prefixed_option(&self, name: &str) -> Option<(&FlagOptionParam, String)> { @@ -487,14 +487,14 @@ impl Command { pub(crate) fn match_version_short_name(&self) -> bool { match self.find_flag_option("-V") { - Some(param) => param.name() == "version", + Some(param) => param.var_name() == "version", None => true, } } pub(crate) fn match_help_short_name(&self) -> bool { match self.find_flag_option("-h") { - Some(param) => param.name() == "help", + Some(param) => param.var_name() == "help", None => true, } } @@ -546,7 +546,7 @@ impl Command { for subcmd in self.subcommands.iter_mut() { let mut inherited_flag_options = vec![]; for flag_option in &self.flag_option_params { - if subcmd.find_flag_option(flag_option.name()).is_none() { + if subcmd.find_flag_option(flag_option.var_name()).is_none() { let mut flag_option = flag_option.clone(); flag_option.inherit = true; inherited_flag_options.push(flag_option); diff --git a/src/command/names_checker.rs b/src/command/names_checker.rs index 81352916..6e099933 100644 --- a/src/command/names_checker.rs +++ b/src/command/names_checker.rs @@ -17,7 +17,7 @@ impl NamesChecker { pos: Position, ) -> Result<()> { let tag_name = param.tag_name(); - let names = param.list_names(); + let names = param.list_option_names(); for name in names.iter() { if let Some((exist_pos, _)) = self.flag_options.get(name) { bail!("{}", Self::conflict_error(tag_name, pos, name, *exist_pos)); @@ -33,7 +33,7 @@ impl NamesChecker { param: &PositionalParam, pos: Position, ) -> Result<()> { - let name = param.name(); + let name = param.var_name(); if let Some(exist_pos) = self.positionals.get(name) { bail!( "{}", diff --git a/src/matcher.rs b/src/matcher.rs index a8e698fd..e64baee1 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -126,11 +126,11 @@ impl<'a, 'b> Matcher<'a, 'b> { if let Some(param) = cmd.find_flag_option(k) { add_param_choice_fn(&mut choice_fns, param); if is_last_arg { - arg_comp = ArgComp::OptionValue(param.name().to_string(), 0); + arg_comp = ArgComp::OptionValue(param.var_name().to_string(), 0); split_last_arg_at = Some(k.len() + 1); } - flag_option_args[cmd_level].push((k, vec![v], Some(param.name()))); - last_flag_option = Some(param.name()); + flag_option_args[cmd_level].push((k, vec![v], Some(param.var_name()))); + last_flag_option = Some(param.var_name()); } else if let Some((param, prefix)) = cmd.find_prefixed_option(arg) { add_param_choice_fn(&mut choice_fns, param); match_prefix_option( @@ -142,7 +142,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut split_last_arg_at, &prefix, ); - last_flag_option = Some(param.name()); + last_flag_option = Some(param.var_name()); } else { flag_option_args[cmd_level].push((k, vec![v], None)); } @@ -157,7 +157,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut split_last_arg_at, combine_shorts, ); - last_flag_option = Some(param.name()); + last_flag_option = Some(param.var_name()); } else if let Some((param, prefix)) = cmd.find_prefixed_option(arg) { add_param_choice_fn(&mut choice_fns, param); match_prefix_option( @@ -169,7 +169,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut split_last_arg_at, &prefix, ); - last_flag_option = Some(param.name()); + last_flag_option = Some(param.var_name()); } else if let Some(subcmd) = find_subcommand(cmd, arg, &positional_args) .and_then(|v| { if is_last_arg && compgen { @@ -224,7 +224,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut split_last_arg_at, combine_shorts, ); - last_flag_option = Some(param.name()); + last_flag_option = Some(param.var_name()); } } else { flag_option_args[cmd_level].push((arg, vec![], None)); @@ -412,7 +412,7 @@ impl<'a, 'b> Matcher<'a, 'b> { if let Some(param) = last_cmd .flag_option_params .iter() - .find(|v| v.name() == name) + .find(|v| v.var_name() == name) { comp_flag_option(param, *index) } else { @@ -464,7 +464,7 @@ impl<'a, 'b> Matcher<'a, 'b> { let values: Vec<&[&str]> = args .iter() .filter_map(|(_, value, name)| { - if let Some(true) = name.map(|v| param.name() == v) { + if let Some(true) = name.map(|v| param.var_name() == v) { Some(value.as_slice()) } else { None @@ -598,7 +598,7 @@ impl<'a, 'b> Matcher<'a, 'b> { .flag_option_params .iter() .filter(|v| v.required()) - .map(|v| v.name()) + .map(|v| v.var_name()) .collect(); for (i, (key, _, name)) in args.iter().enumerate() { match *name { @@ -617,7 +617,7 @@ impl<'a, 'b> Matcher<'a, 'b> { missing_params.extend(missing_flag_options) } for (name, indexes) in flag_option_map { - if let Some(param) = cmd.flag_option_params.iter().find(|v| v.name() == name) { + if let Some(param) = cmd.flag_option_params.iter().find(|v| v.var_name() == name) { let values_list: Vec<&[&str]> = indexes.iter().map(|v| args[*v].1.as_slice()).collect(); if !param.multi_occurs() && values_list.len() > 1 { @@ -866,7 +866,7 @@ impl<'a, 'b> Matcher<'a, 'b> { .collect(); let last = self.args.last().map(|v| v.as_str()).unwrap_or_default(); for param in cmd.flag_option_params.iter() { - let mut exist = args.contains(param.name()); + let mut exist = args.contains(param.var_name()); if !last.is_empty() && param.is_match(last) { exist = false; } @@ -877,7 +877,7 @@ impl<'a, 'b> Matcher<'a, 'b> { } else { CompColor::of_option() }; - for v in param.list_names() { + for v in param.list_option_names() { output.push((v, describe.to_string(), param.prefixed().is_some(), kind)) } } @@ -966,7 +966,7 @@ fn match_combine_shorts<'a, 'b>( } } if let Some(param) = current_cmd.find_flag_option(&name) { - output.push((arg, vec![], Some(param.name()))) + output.push((arg, vec![], Some(param.var_name()))) } else { return None; } @@ -989,10 +989,12 @@ fn match_flag_option<'a, 'b>( let arg = &args[*arg_index]; *arg_index += value_args.len(); if !value_args.is_empty() { - *arg_comp = - ArgComp::OptionValue(param.name().to_string(), value_args.len().saturating_sub(1)); + *arg_comp = ArgComp::OptionValue( + param.var_name().to_string(), + value_args.len().saturating_sub(1), + ); } - flag_option_args.push((arg, value_args, Some(param.name()))); + flag_option_args.push((arg, value_args, Some(param.var_name()))); } else { let mut values_len = param.arg_value_names.len(); if param.unlimited_args() { @@ -1006,20 +1008,20 @@ fn match_flag_option<'a, 'b>( if *arg_comp != ArgComp::FlagOrOption { if param.is_option() && value_args.len() <= values_len { *arg_comp = ArgComp::OptionValue( - param.name().to_string(), + param.var_name().to_string(), value_args.len().saturating_sub(1), ); } } else if let Some(prefix) = param.prefixed() { if arg.starts_with(&prefix) { - *arg_comp = ArgComp::OptionValue(param.name().to_string(), 0); + *arg_comp = ArgComp::OptionValue(param.var_name().to_string(), 0); *split_last_arg_at = Some(prefix.len()); } } else if combine_shorts && param.is_flag() && !(arg.len() > 2 && param.is_match(arg)) { *arg_comp = ArgComp::FlagOrOptionCombine(arg.to_string()); } } - flag_option_args.push((arg, value_args, Some(param.name()))); + flag_option_args.push((arg, value_args, Some(param.var_name()))); } } @@ -1036,10 +1038,10 @@ fn match_prefix_option<'a, 'b>( let args_len = args.len(); let arg = &args[*arg_index]; if *arg_index == args_len - 1 { - *arg_comp = ArgComp::OptionValue(param.name().to_string(), 0); + *arg_comp = ArgComp::OptionValue(param.var_name().to_string(), 0); *split_last_arg_at = Some(prefix_len); } - flag_option_args.push((arg, vec![&arg[prefix_len..]], Some(param.name()))); + flag_option_args.push((arg, vec![&arg[prefix_len..]], Some(param.var_name()))); } fn match_command<'a, 'b>( diff --git a/src/param.rs b/src/param.rs index 1abe7188..0b3809cd 100644 --- a/src/param.rs +++ b/src/param.rs @@ -14,6 +14,7 @@ pub(crate) struct FlagOptionParam { pub(crate) sign: char, pub(crate) single: bool, pub(crate) data: ParamData, + pub(crate) var_name: String, pub(crate) value_names: Vec, pub(crate) arg_value_names: Vec, pub(crate) inherit: bool, @@ -29,12 +30,17 @@ impl FlagOptionParam { single: bool, value_names: &[&str], ) -> Self { - let name = param.name.clone(); + let param_name = param.name.clone(); + let var_name = if sign == '+' { + format!("plus_{}", param_name) + } else { + param_name.clone() + }; let value_names: Vec = value_names.iter().map(|v| v.to_string()).collect(); let mut arg_value_names = if flag { vec![] } else if value_names.is_empty() { - vec![to_cobol_case(&name)] + vec![to_cobol_case(¶m_name)] } else { value_names.iter().map(|v| to_cobol_case(v)).collect() }; @@ -49,14 +55,15 @@ impl FlagOptionParam { sign, single, data: param, + var_name, value_names, arg_value_names, inherit: false, } } - pub(crate) fn name(&self) -> &str { - self.data.name.as_str() + pub(crate) fn var_name(&self) -> &str { + &self.var_name } pub(crate) fn is_flag(&self) -> bool { @@ -169,7 +176,7 @@ impl FlagOptionParam { } pub(crate) fn render_name(&self) -> String { - format!("{}{}", self.render_long_prefix(), self.name()) + format!("{}{}", self.render_long_prefix(), self.data.name) } pub(crate) fn render_first_notation(&self) -> String { @@ -188,8 +195,8 @@ impl FlagOptionParam { pub(crate) fn render_body(&self) -> String { let mut output = String::new(); let sign = self.sign; - if self.single && self.short.is_none() && self.name().len() == 1 { - output.push_str(&format!("{sign}{}", self.name())); + if self.single && self.short.is_none() && self.data.name.len() == 1 { + output.push_str(&format!("{sign}{}", self.data.name)); } else { if let Some(ch) = self.short { output.push_str(&format!("{sign}{ch}, ")) @@ -197,7 +204,7 @@ impl FlagOptionParam { output.push_str(" ") }; output.push_str(&format!("{:>2}", self.render_long_prefix())); - output.push_str(self.name()); + output.push_str(&self.data.name); } if self.is_flag() { @@ -240,24 +247,21 @@ impl FlagOptionParam { } pub(crate) fn get_arg_value(&self, values: &[&[&str]]) -> Option { - let mut name = self.name().to_string(); - if self.sign == '+' { - name = format!("plus_{name}") - } + let var_name = self.var_name().to_string(); if self.flag { if values.is_empty() { None } else { - Some(ArgcValue::Single(name, values.len().to_string())) + Some(ArgcValue::Single(var_name, values.len().to_string())) } } else { if values.is_empty() { match &self.data.default { Some(DefaultData::Value(value)) => { - return Some(ArgcValue::Single(name, value.clone())); + return Some(ArgcValue::Single(var_name, value.clone())); } Some(DefaultData::Fn(f)) => { - return Some(ArgcValue::SingleFn(name, f.clone())); + return Some(ArgcValue::SingleFn(var_name, f.clone())); } None => return None, } @@ -278,20 +282,20 @@ impl FlagOptionParam { }) .collect() } - Some(ArgcValue::Multiple(name, values)) + Some(ArgcValue::Multiple(var_name, values)) } else if self.arg_value_names.len() > 1 { Some(ArgcValue::Multiple( - name, + var_name, values[0].iter().map(|v| v.to_string()).collect(), )) } else { - Some(ArgcValue::Single(name, must_get_first(values[0]))) + Some(ArgcValue::Single(var_name, must_get_first(values[0]))) } } } pub(crate) fn is_match(&self, name: &str) -> bool { - self.list_names().iter().any(|v| v == name) + self.list_option_names().iter().any(|v| v == name) } pub(crate) fn prefixed(&self) -> Option { @@ -309,9 +313,9 @@ impl FlagOptionParam { Some(self.render_name()) } - pub(crate) fn list_names(&self) -> Vec { + pub(crate) fn list_option_names(&self) -> Vec { let mut output = vec![]; - output.push(format!("{}{}", self.render_long_prefix(), self.name())); + output.push(format!("{}{}", self.render_long_prefix(), self.data.name)); if let Some(short) = self.short { output.push(format!("{}{}", self.sign, short)); } @@ -326,9 +330,9 @@ impl FlagOptionParam { } pub fn to_json(&self) -> serde_json::Value { - let option_names = self.list_names(); + let option_names = self.list_option_names(); json!({ - "name": self.name(), + "name": self.data.name, "describe": self.describe, "flag": self.flag, "option_names": option_names, @@ -362,7 +366,7 @@ impl PositionalParam { } } - pub(crate) fn name(&self) -> &str { + pub(crate) fn var_name(&self) -> &str { &self.data.name } @@ -422,7 +426,7 @@ impl PositionalParam { } pub(crate) fn get_arg_value(&self, values: &[&str]) -> Option { - let name = self.name().to_string(); + let name = self.var_name().to_string(); if values.is_empty() { match &self.data.default { Some(DefaultData::Value(value)) => { @@ -462,7 +466,7 @@ impl PositionalParam { pub fn to_json(&self) -> serde_json::Value { json!({ - "name": self.name(), + "name": self.var_name(), "describe": self.describe, "modifier": self.data.modifer, "choices": self.data.choice_values(), From 3560ea3fffd5bfd20ea4e9bbdea56de31629a370 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 10 Nov 2023 09:13:16 +0800 Subject: [PATCH 3/4] make matcher support + option, add tests --- examples/options.sh | 52 +++++++++++++++++++ src/command/mod.rs | 8 +++ src/matcher.rs | 30 ++++++++--- tests/compgen.rs | 20 +++++++ .../integration__compgen__plus_sign.snap | 21 ++++++++ .../integration__spec__plus_sign.snap | 16 ++++++ tests/spec.rs | 13 +++++ 7 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 tests/snapshots/integration__compgen__plus_sign.snap create mode 100644 tests/snapshots/integration__spec__plus_sign.snap diff --git a/examples/options.sh b/examples/options.sh index 5c9a7636..1deb5328 100755 --- a/examples/options.sh +++ b/examples/options.sh @@ -48,6 +48,58 @@ flags() { :; } + +# @cmd +# @describe All kind of options +# @option +oa +# @option +b +ob short +# @option +c short only +# @option +oc! required +# @option +od* multi-occurs +# @option +oe+ required + multi-occurs +# @option +ona value notation +# @option +onb two-args value notations +# @option +onc unlimited-args value notations +# @option +oda=a default +# @option +odb=`_default_fn` default from fn +# @option +oca[a|b] choice +# @option +ocb[=a|b] choice + default +# @option +occ*[a|b] multi-occurs + choice +# @option +ocd+[a|b] required + multi-occurs + choice +# @option +ofa[`_choice_fn`] choice from fn +# @option +ofb[?`_choice_fn`] choice from fn + no validation +# @option +ofc*[`_choice_fn`] multi-occurs + choice from fn +# @option +oxa~ capture all remaining args +plus_options() { + :; +} + + +# @cmd +# @describe All kind of flags +# @flag +fa +# @flag +b +fb short +# @flag +c short only +# @flag +fd* multi-occurs +# @flag +e +fe* short + multi-occurs +plus_flags() { + :; +} + +# @cmd +# @describe Flags or options with single dash +# @flag +fa +# @flag +b +fb +# @flag +fd* +# @option +oa +# @option +od* +# @option +ona +# @option +oca[a|b] +# @option +ofa[`_choice_fn`] +plus_1dash() { + :; +} + _default_fn() { whoami } diff --git a/src/command/mod.rs b/src/command/mod.rs index b911b4ac..496db40d 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -271,6 +271,14 @@ impl Command { self.metadata.iter().any(|(k, _, _)| k == key) } + pub(crate) fn flag_option_signs(&self) -> &str { + if self.flag_option_params.iter().any(|v| v.sign == '+') { + "+-" + } else { + "-" + } + } + pub(crate) fn render_help(&self, cmd_paths: &[&str], term_width: Option) -> String { let mut output = vec![]; if self.version.is_some() { diff --git a/src/matcher.rs b/src/matcher.rs index e64baee1..ce5b173d 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_arguments)] + use std::{ collections::{HashMap, HashSet}, env, @@ -86,7 +88,7 @@ impl<'a, 'b> Matcher<'a, 'b> { } else { let mut is_rest_args_positional = false; // option(e.g. -f --foo) will be treated as positional arg if let Some(arg) = args.last() { - if arg.starts_with('-') { + if arg.starts_with(|c| root.flag_option_signs().contains(c)) { arg_comp = ArgComp::FlagOrOption; } else if !arg.is_empty() { arg_comp = ArgComp::CommandOrPositional; @@ -94,6 +96,7 @@ impl<'a, 'b> Matcher<'a, 'b> { } while arg_index < args_len { let cmd = cmds[cmd_level].1; + let signs = cmd.flag_option_signs(); let arg = args[arg_index].as_str(); let is_last_arg = arg_index == args_len - 1; last_flag_option = None; @@ -121,7 +124,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut is_rest_args_positional, cmd, ); - } else if arg.starts_with('-') { + } else if arg.starts_with(|c| signs.contains(c)) { if let Some((k, v)) = arg.split_once('=') { if let Some(param) = cmd.find_flag_option(k) { add_param_choice_fn(&mut choice_fns, param); @@ -156,6 +159,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut arg_comp, &mut split_last_arg_at, combine_shorts, + signs, ); last_flag_option = Some(param.var_name()); } else if let Some((param, prefix)) = cmd.find_prefixed_option(arg) { @@ -223,9 +227,19 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut arg_comp, &mut split_last_arg_at, combine_shorts, + signs, ); last_flag_option = Some(param.var_name()); } + } else if let Some((ch, symbol_param)) = find_symbol(cmd, arg) { + if let Some(choice_fn) = &symbol_param.1 { + choice_fns.insert(choice_fn); + } + symbol_args.push((&arg[1..], symbol_param)); + if is_last_arg { + arg_comp = ArgComp::Symbol(ch); + split_last_arg_at = Some(1); + } } else { flag_option_args[cmd_level].push((arg, vec![], None)); } @@ -927,14 +941,14 @@ fn add_positional_arg<'a>( } } -fn take_value_args(args: &[String], start: usize, len: usize) -> Vec<&str> { +fn take_value_args<'a>(args: &'a [String], start: usize, len: usize, signs: &str) -> Vec<&'a str> { let mut output = vec![]; if len == 0 { return output; } let end = (start + len).min(args.len()); for arg in args.iter().take(end).skip(start) { - if arg.starts_with('-') { + if arg.starts_with(|c| signs.contains(c)) { break; } output.push(arg.as_str()); @@ -983,6 +997,7 @@ fn match_flag_option<'a, 'b>( arg_comp: &mut ArgComp, split_last_arg_at: &mut Option, combine_shorts: bool, + signs: &str, ) { if param.terminated() { let value_args: Vec<&str> = args[*arg_index + 1..].iter().map(|v| v.as_str()).collect(); @@ -1001,7 +1016,7 @@ fn match_flag_option<'a, 'b>( values_len = usize::MAX / 2; } let args_len = args.len(); - let value_args = take_value_args(args, *arg_index + 1, values_len); + let value_args = take_value_args(args, *arg_index + 1, values_len, signs); let arg = &args[*arg_index]; *arg_index += value_args.len(); if *arg_index == args_len - 1 { @@ -1089,13 +1104,16 @@ fn comp_subcomands(cmd: &Command, flag: bool) -> Vec { let mut output = vec![]; let mut has_help_subcmd = false; let mut describe_help_subcmd = false; + let signs = cmd.flag_option_signs(); for subcmd in cmd.subcommands.iter() { let describe = subcmd.describe_oneline(); for (i, v) in subcmd.list_names().into_iter().enumerate() { if i > 0 && v.len() < 2 { continue; } - if (flag && v.starts_with('-')) || (!flag && !v.starts_with('-')) { + if (flag && v.starts_with(|c| signs.contains(c))) + || (!flag && !v.starts_with(|c| signs.contains(c))) + { if !flag { has_help_subcmd = true; } diff --git a/tests/compgen.rs b/tests/compgen.rs index bb0d6e20..7ead1cab 100644 --- a/tests/compgen.rs +++ b/tests/compgen.rs @@ -98,6 +98,26 @@ _choice_fn() { ); } +#[test] +fn plus_sign() { + let script = r#" +# @flag +a +# @option +fb[abc|def|ijk] +# @option +c +fc*[`_choice_fn`] +_choice_fn() { + echo -e "abc\ndef\nghi" +} +"#; + snapshot_compgen!( + script, + [ + vec!["prog", "+"], + vec!["prog", "+fb="], + vec!["prog", "+fc", ""], + ] + ); +} + #[test] fn subcmds() { const SCRIPT: &str = r###" diff --git a/tests/snapshots/integration__compgen__plus_sign.snap b/tests/snapshots/integration__compgen__plus_sign.snap new file mode 100644 index 00000000..83790b34 --- /dev/null +++ b/tests/snapshots/integration__compgen__plus_sign.snap @@ -0,0 +1,21 @@ +--- +source: tests/compgen.rs +expression: data +--- +************ COMPGEN `prog +` ************ ++a /color:cyan ++fb /color:cyan,bold ++fc /color:cyan,bold ++c /color:cyan,bold + +************ COMPGEN `prog +fb=` ************ +abc /color:default +def /color:default +ijk /color:default + +************ COMPGEN `prog +fc ` ************ +abc /color:default +def /color:default +ghi /color:default + + diff --git a/tests/snapshots/integration__spec__plus_sign.snap b/tests/snapshots/integration__spec__plus_sign.snap new file mode 100644 index 00000000..44122c4c --- /dev/null +++ b/tests/snapshots/integration__spec__plus_sign.snap @@ -0,0 +1,16 @@ +--- +source: tests/spec.rs +expression: data +--- +************ RUN ************ +prog +a +fb fb +c fc1 +fc fc2 + +OUTPUT +argc_plus_a=1 +argc_plus_fb=fb +argc_plus_fc=( fc1 fc2 ) +argc__args=( prog +a +fb fb +c fc1 +fc fc2 ) +argc__cmd_arg_index=0 +argc__positionals=( ) + + diff --git a/tests/spec.rs b/tests/spec.rs index 369328de..32846f5f 100644 --- a/tests/spec.rs +++ b/tests/spec.rs @@ -314,3 +314,16 @@ fn symbol() { "###; snapshot_multi!(script, [vec!["prog", "+nightly"]]); } + +#[test] +fn plus_sign() { + let script = r###" +# @flag +a +# @option +fb +# @option +c +fc* +"###; + snapshot_multi!( + script, + [vec!["prog", "+a", "+fb", "fb", "+c", "fc1", "+fc", "fc2"]] + ); +} From 54a38fc2f9f0312e57946f62ea61d496b34047cd Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 10 Nov 2023 11:02:27 +0800 Subject: [PATCH 4/4] allow mix -/+ --- examples/options.sh | 25 +++--- src/command/mod.rs | 18 ++-- src/matcher.rs | 4 +- src/param.rs | 58 +++++------- src/parser.rs | 90 ++++++++----------- tests/compgen.rs | 2 + .../integration__compgen__plus_sign.snap | 6 ++ .../integration__spec__plus_sign.snap | 9 ++ tests/spec.rs | 6 +- 9 files changed, 106 insertions(+), 112 deletions(-) diff --git a/examples/options.sh b/examples/options.sh index 1deb5328..53c92ef3 100755 --- a/examples/options.sh +++ b/examples/options.sh @@ -1,5 +1,4 @@ -# @cmd -# @describe All kind of options +# @cmd All kind of options # @option --oa # @option -b --ob short # @option -c short only @@ -23,8 +22,7 @@ options() { :; } -# @cmd -# @describe All kind of flags +# @cmd All kind of flags # @flag --fa # @flag -b --fb short # @flag -c short only @@ -34,8 +32,7 @@ flags() { :; } -# @cmd -# @describe Flags or options with single dash +# @cmd Flags or options with single dash # @flag -fa # @flag -b -fb # @flag -fd* @@ -49,8 +46,7 @@ flags() { } -# @cmd -# @describe All kind of options +# @cmd All kind of options # @option +oa # @option +b +ob short # @option +c short only @@ -75,8 +71,7 @@ plus_options() { } -# @cmd -# @describe All kind of flags +# @cmd All kind of flags # @flag +fa # @flag +b +fb short # @flag +c short only @@ -86,8 +81,7 @@ plus_flags() { :; } -# @cmd -# @describe Flags or options with single dash +# @cmd Flags or options with single dash # @flag +fa # @flag +b +fb # @flag +fd* @@ -100,6 +94,13 @@ plus_1dash() { :; } + +# @cmd Mixed `-` and `+` options +# @option +b --ob +mix_options() { + :; +} + _default_fn() { whoami } diff --git a/src/command/mod.rs b/src/command/mod.rs index 496db40d..a5456ce8 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -14,7 +14,7 @@ use crate::Result; use anyhow::{anyhow, bail}; use indexmap::IndexMap; use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; pub fn eval( @@ -271,12 +271,16 @@ impl Command { self.metadata.iter().any(|(k, _, _)| k == key) } - pub(crate) fn flag_option_signs(&self) -> &str { - if self.flag_option_params.iter().any(|v| v.sign == '+') { - "+-" - } else { - "-" + pub(crate) fn flag_option_signs(&self) -> String { + let mut signs = HashSet::new(); + signs.insert('-'); + for param in &self.flag_option_params { + if let Some(short) = ¶m.short { + signs.extend(short.chars().take(1)) + } + signs.extend(param.long_prefix.chars()) } + signs.into_iter().collect() } pub(crate) fn render_help(&self, cmd_paths: &[&str], term_width: Option) -> String { @@ -371,7 +375,7 @@ impl Command { let mut any_describe = false; let mut single = false; for param in self.flag_option_params.iter() { - if param.single { + if param.long_prefix.len() == 1 { single = true; } let value = param.render_body(); diff --git a/src/matcher.rs b/src/matcher.rs index ce5b173d..4cf072b6 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -159,7 +159,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut arg_comp, &mut split_last_arg_at, combine_shorts, - signs, + &signs, ); last_flag_option = Some(param.var_name()); } else if let Some((param, prefix)) = cmd.find_prefixed_option(arg) { @@ -227,7 +227,7 @@ impl<'a, 'b> Matcher<'a, 'b> { &mut arg_comp, &mut split_last_arg_at, combine_shorts, - signs, + &signs, ); last_flag_option = Some(param.var_name()); } diff --git a/src/param.rs b/src/param.rs index 0b3809cd..1958f97b 100644 --- a/src/param.rs +++ b/src/param.rs @@ -9,10 +9,9 @@ use serde_json::json; #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct FlagOptionParam { pub(crate) describe: String, - pub(crate) short: Option, pub(crate) flag: bool, - pub(crate) sign: char, - pub(crate) single: bool, + pub(crate) short: Option, + pub(crate) long_prefix: String, pub(crate) data: ParamData, pub(crate) var_name: String, pub(crate) value_names: Vec, @@ -24,14 +23,13 @@ impl FlagOptionParam { pub(crate) fn new( param: ParamData, describe: &str, - short: Option, flag: bool, - sign: char, - single: bool, + short: Option<&str>, + long_prefix: &str, value_names: &[&str], ) -> Self { let param_name = param.name.clone(); - let var_name = if sign == '+' { + let var_name = if long_prefix.starts_with('+') { format!("plus_{}", param_name) } else { param_name.clone() @@ -50,10 +48,9 @@ impl FlagOptionParam { } Self { describe: describe.to_string(), - short, flag, - sign, - single, + short: short.map(|v| v.to_string()), + long_prefix: long_prefix.to_string(), data: param, var_name, value_names, @@ -146,12 +143,12 @@ impl FlagOptionParam { #[allow(unused)] pub(crate) fn render_source(&self) -> String { let mut output = vec![]; - if let Some(ch) = self.short { - output.push(format!("{}{}", self.sign, ch)); + if let Some(short) = &self.short { + output.push(short.to_string()); }; output.push(format!( "{}{}", - self.render_long_prefix(), + self.long_prefix, self.data.render_name_value() )); for value_name in &self.value_names { @@ -163,20 +160,8 @@ impl FlagOptionParam { output.join(" ") } - pub(crate) fn render_long_prefix(&self) -> &str { - if self.single { - if self.sign == '+' { - "+" - } else { - "-" - } - } else { - "--" - } - } - pub(crate) fn render_name(&self) -> String { - format!("{}{}", self.render_long_prefix(), self.data.name) + format!("{}{}", self.long_prefix, self.data.name) } pub(crate) fn render_first_notation(&self) -> String { @@ -194,16 +179,15 @@ impl FlagOptionParam { pub(crate) fn render_body(&self) -> String { let mut output = String::new(); - let sign = self.sign; - if self.single && self.short.is_none() && self.data.name.len() == 1 { - output.push_str(&format!("{sign}{}", self.data.name)); + if self.short.is_none() && self.long_prefix.len() == 1 && self.data.name.len() == 1 { + output.push_str(&self.render_name()); } else { - if let Some(ch) = self.short { - output.push_str(&format!("{sign}{ch}, ")) + if let Some(short) = &self.short { + output.push_str(&format!("{short}, ")) } else { output.push_str(" ") }; - output.push_str(&format!("{:>2}", self.render_long_prefix())); + output.push_str(&format!("{:>2}", self.long_prefix)); output.push_str(&self.data.name); } @@ -306,8 +290,8 @@ impl FlagOptionParam { return None; } - if let Some(ch) = self.short { - return Some(format!("{}{ch}", self.sign)); + if let Some(short) = &self.short { + return Some(short.clone()); } Some(self.render_name()) @@ -315,9 +299,9 @@ impl FlagOptionParam { pub(crate) fn list_option_names(&self) -> Vec { let mut output = vec![]; - output.push(format!("{}{}", self.render_long_prefix(), self.data.name)); - if let Some(short) = self.short { - output.push(format!("{}{}", self.sign, short)); + output.push(self.render_name()); + if let Some(short) = &self.short { + output.push(short.clone()); } output } diff --git a/src/parser.rs b/src/parser.rs index 6741c5f2..a16b7951 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,8 +4,6 @@ use crate::param::{ use crate::utils::{is_choice_value_terminate, is_default_value_terminate}; use crate::Result; use anyhow::bail; -use nom::bytes::complete::is_not; -use nom::error::ErrorKind; use nom::{ branch::alt, bytes::complete::{escaped, tag, take_till, take_while1}, @@ -14,6 +12,7 @@ use nom::{ streaming::none_of, }, combinator::{eof, fail, map, not, opt, peek, rest, success}, + error::ErrorKind, multi::{many0, many1, separated_list1}, sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}, }; @@ -235,8 +234,8 @@ fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionPar parse_zero_or_many_value_notations, parse_tail, )), - |((sign, short, single), arg, value_names, describe)| { - FlagOptionParam::new(arg, describe, short, false, sign, single, &value_names) + |((short, long_prefix), arg, value_names, describe)| { + FlagOptionParam::new(arg, describe, false, short, long_prefix, &value_names) }, )(input) } @@ -245,7 +244,7 @@ fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionPar fn parse_no_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( tuple(( - preceded(space0, alt((char('-'), char('+')))), + preceded(space0, alt((tag("-"), tag("+")))), preceded( verify_single_char, alt(( @@ -260,8 +259,8 @@ fn parse_no_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam parse_zero_or_many_value_notations, parse_tail, )), - |(sign, arg, value_names, describe)| { - FlagOptionParam::new(arg, describe, None, false, sign, true, &value_names) + |(long_prefix, arg, value_names, describe)| { + FlagOptionParam::new(arg, describe, false, None, long_prefix, &value_names) }, )(input) } @@ -293,13 +292,9 @@ fn parse_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { // Parse `@flag` fn parse_with_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( - tuple(( - parse_with_long_head, - parse_long_flag_and_asterisk, - parse_tail, - )), - |((sign, short, single), arg, describe)| { - FlagOptionParam::new(arg, describe, short, true, sign, single, &[]) + tuple((parse_with_long_head, parse_with_long_flag_name, parse_tail)), + |((short, long_prefix), arg, describe)| { + FlagOptionParam::new(arg, describe, true, short, long_prefix, &[]) }, )(input) } @@ -308,16 +303,18 @@ fn parse_with_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam fn parse_no_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( tuple(( - preceded(space0, alt((char('-'), char('+')))), - parse_short_flag_and_asterisk, + preceded(space0, alt((tag("-"), tag("+")))), + parse_no_long_flag_name, parse_tail, )), - |(sign, arg, describe)| FlagOptionParam::new(arg, describe, None, true, sign, true, &[]), + |(long_prefix, arg, describe)| { + FlagOptionParam::new(arg, describe, true, None, long_prefix, &[]) + }, )(input) } // Parse `str*` `str` -fn parse_long_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { +fn parse_with_long_flag_name(input: &str) -> nom::IResult<&str, ParamData> { alt(( map(terminated(parse_param_name, tag("*")), |mut arg| { arg.modifer = Modifier::MultipleOptional; @@ -328,7 +325,7 @@ fn parse_long_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { } // Parse 'ch*' or 'ch` -fn parse_short_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { +fn parse_no_long_flag_name(input: &str) -> nom::IResult<&str, ParamData> { fn parser(input: &str) -> nom::IResult<&str, ParamData> { map(satisfy(is_short_char), |ch| { ParamData::new(&format!("{}", ch)) @@ -342,28 +339,19 @@ fn parse_short_flag_and_asterisk(input: &str) -> nom::IResult<&str, ParamData> { })(input) } -fn parse_with_long_head(input: &str) -> nom::IResult<&str, (char, Option, bool)> { +fn parse_with_long_head(input: &str) -> nom::IResult<&str, (Option<&str>, &str)> { map( - alt(( - pair( - opt(terminated( - pair(char::<&str, _>('-'), satisfy(is_short_char)), - peek(space1), - )), - preceded(space0, alt((tag("--"), tag("-")))), - ), - pair( - opt(terminated( - pair(char::<&str, _>('+'), satisfy(is_short_char)), - peek(space1), - )), - terminated(preceded(space0, tag("+")), peek(is_not("+"))), - ), - )), - |(short, long)| match short { - Some((sign, short)) => (sign, Some(short), long.len() == 1), - None => (long.chars().next().unwrap(), None, long.len() == 1), - }, + alt((pair( + opt(terminated( + pair( + opt(alt((char::<&str, _>('-'), char('+')))), + satisfy(is_short_char), + ), + peek(space1), + )), + preceded(space0, alt((tag("--"), tag("-"), tag("+")))), + ),)), + |(short, long_prefix)| (short.map(|_| &input[0..2]), long_prefix), )(input) } @@ -690,10 +678,10 @@ fn is_name_char(c: char) -> bool { } fn is_short_char(c: char) -> bool { - c.is_ascii() - && !matches!( + c.is_ascii_graphic() + || !matches!( c, - '-' | '\t' | '"' | '\'' | '(' | ')' | '[' | ']' | '<' | '>' | '&' | '\\' | ';' | '|' + '"' | '&' | '\'' | '(' | ')' | '-' | ';' | '<' | '>' | '\\' | '`' | '|' ) } @@ -940,23 +928,19 @@ mod tests { fn test_parse_with_long_head() { assert_eq!( parse_with_long_head("-f --foo"), - Ok(("foo", ('-', Some('f'), false))) + Ok(("foo", (Some("-f"), "--"))) ); assert_eq!( parse_with_long_head("-f -foo"), - Ok(("foo", ('-', Some('f'), true))) - ); - assert_eq!( - parse_with_long_head("--foo"), - Ok(("foo", ('-', None, false))) + Ok(("foo", (Some("-f"), "-"))) ); - assert_eq!(parse_with_long_head("-foo"), Ok(("foo", ('-', None, true)))); + assert_eq!(parse_with_long_head("--foo"), Ok(("foo", (None, "--")))); + assert_eq!(parse_with_long_head("-foo"), Ok(("foo", (None, "-")))); assert_eq!( parse_with_long_head("+f +foo"), - Ok(("foo", ('+', Some('f'), true))) + Ok(("foo", (Some("+f"), "+"))) ); - assert_eq!(parse_with_long_head("+foo"), Ok(("foo", ('+', None, true)))); - assert!(parse_with_long_head("++foo").is_err()); + assert_eq!(parse_with_long_head("+foo"), Ok(("foo", (None, "+")))); } #[test] diff --git a/tests/compgen.rs b/tests/compgen.rs index 7ead1cab..c2222dc9 100644 --- a/tests/compgen.rs +++ b/tests/compgen.rs @@ -104,6 +104,7 @@ fn plus_sign() { # @flag +a # @option +fb[abc|def|ijk] # @option +c +fc*[`_choice_fn`] +# @option +d -fd*[`_choice_fn`] _choice_fn() { echo -e "abc\ndef\nghi" } @@ -114,6 +115,7 @@ _choice_fn() { vec!["prog", "+"], vec!["prog", "+fb="], vec!["prog", "+fc", ""], + vec!["prog", "-fd", ""], ] ); } diff --git a/tests/snapshots/integration__compgen__plus_sign.snap b/tests/snapshots/integration__compgen__plus_sign.snap index 83790b34..fecf0101 100644 --- a/tests/snapshots/integration__compgen__plus_sign.snap +++ b/tests/snapshots/integration__compgen__plus_sign.snap @@ -7,6 +7,7 @@ expression: data +fb /color:cyan,bold +fc /color:cyan,bold +c /color:cyan,bold ++d /color:cyan,bold ************ COMPGEN `prog +fb=` ************ abc /color:default @@ -18,4 +19,9 @@ abc /color:default def /color:default ghi /color:default +************ COMPGEN `prog -fd ` ************ +abc /color:default +def /color:default +ghi /color:default + diff --git a/tests/snapshots/integration__spec__plus_sign.snap b/tests/snapshots/integration__spec__plus_sign.snap index 44122c4c..ad1e6f54 100644 --- a/tests/snapshots/integration__spec__plus_sign.snap +++ b/tests/snapshots/integration__spec__plus_sign.snap @@ -13,4 +13,13 @@ argc__args=( prog +a +fb fb +c fc1 +fc fc2 ) argc__cmd_arg_index=0 argc__positionals=( ) +************ RUN ************ +prog +d fd1 -fd fd2 + +OUTPUT +argc_fd=( fd1 fd2 ) +argc__args=( prog +d fd1 -fd fd2 ) +argc__cmd_arg_index=0 +argc__positionals=( ) + diff --git a/tests/spec.rs b/tests/spec.rs index 32846f5f..7bf3866b 100644 --- a/tests/spec.rs +++ b/tests/spec.rs @@ -321,9 +321,13 @@ fn plus_sign() { # @flag +a # @option +fb # @option +c +fc* +# @option +d -fd* "###; snapshot_multi!( script, - [vec!["prog", "+a", "+fb", "fb", "+c", "fc1", "+fc", "fc2"]] + [ + vec!["prog", "+a", "+fb", "fb", "+c", "fc1", "+fc", "fc2"], + vec!["prog", "+d", "fd1", "-fd", "fd2"], + ] ); }