diff --git a/src/command/mod.rs b/src/command/mod.rs index 333f7c6b..e2cc0a00 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -7,11 +7,11 @@ use self::root_data::RootData; use crate::argc_value::ArgcValue; use crate::matcher::Matcher; use crate::param::{FlagOptionParam, PositionalParam}; -use crate::parser::{parse, Event, EventData, EventScope, Position}; +use crate::parser::{parse, parse_symbol, Event, EventData, EventScope, Position}; use crate::utils::INTERNAL_MODE; use crate::Result; -use anyhow::bail; +use anyhow::{anyhow, bail}; use indexmap::IndexMap; use std::cell::RefCell; use std::collections::HashMap; @@ -48,14 +48,15 @@ pub struct Command { pub(crate) names_checker: NamesChecker, pub(crate) root: Arc>, pub(crate) aliases: Vec, - pub(crate) metadata: IndexMap, + pub(crate) metadata: Vec<(String, String, Position)>, + pub(crate) symbols: IndexMap, } impl Command { pub fn new(source: &str) -> Result { let events = parse(source)?; let mut root = Command::new_from_events(&events)?; - if root.metadata.contains_key("inherit-flag-options") { + if root.has_metadata("inherit-flag-options") { root.inherit_flag_options(); } Ok(root) @@ -102,11 +103,8 @@ impl Command { .collect(); let positional_params: Vec = self.positional_params.iter().map(|v| v.to_json()).collect(); - let mut metadata = serde_json::Map::new(); - for (k, (v, _)) in &self.metadata { - metadata.insert(k.to_string(), serde_json::Value::String(v.to_string())); - } - let metadata = serde_json::Value::Object(metadata); + let metadata: Vec> = + self.metadata.iter().map(|(k, v, _)| vec![k, v]).collect(); serde_json::json!({ "describe": self.describe, "name": self.name, @@ -140,15 +138,14 @@ impl Command { } EventData::Meta(key, value) => { let cmd = Self::get_cmd(&mut root_cmd, "@meta", position)?; - if let Some((_, pos)) = cmd.metadata.get(&key) { - bail!( - "@meta(line {}) conflicts with '{}' at line {}", - position, - key, - pos - ) + if key == "symbol" { + let (ch, name, choice_fn) = parse_symbol(&value).ok_or_else(|| { + anyhow!("@meta(line {}) invalid symbol value", position) + })?; + cmd.symbols + .insert(ch, (name.to_string(), choice_fn.map(|v| v.to_string()))); } - cmd.metadata.insert(key, (value, position)); + cmd.metadata.push((key, value, position)); } EventData::Cmd(value) => { if root_data.borrow().scope == EventScope::CmdStart { @@ -270,6 +267,10 @@ impl Command { Ok(root_cmd) } + pub(crate) fn has_metadata(&self, key: &str) -> bool { + self.metadata.iter().any(|(k, _, _)| k == key) + } + pub(crate) fn render_help(&self, cmd_paths: &[&str], term_width: Option) -> String { let mut output = vec![]; if self.version.is_some() { @@ -644,6 +645,8 @@ impl Command { } } +pub(crate) type SymbolParam = (String, Option); + fn retrive_cmd<'a>(cmd: &'a mut Command, cmd_paths: &[String]) -> Option<&'a mut Command> { if cmd_paths.is_empty() { return Some(cmd); diff --git a/src/matcher.rs b/src/matcher.rs index 3e796e65..a8e698fd 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::{ - command::Command, + command::{Command, SymbolParam}, compgen::CompColor, param::{ChoiceData, FlagOptionParam, ParamData, PositionalParam}, utils::run_param_fns, @@ -22,6 +22,7 @@ pub(crate) struct Matcher<'a, 'b> { args: &'b [String], flag_option_args: Vec>>, positional_args: Vec<&'b str>, + symbol_args: Vec>, dashes: Option, arg_comp: ArgComp, choice_fns: HashSet<&'a str>, @@ -32,6 +33,7 @@ pub(crate) struct Matcher<'a, 'b> { } type FlagOptionArg<'a, 'b> = (&'b str, Vec<&'b str>, Option<&'a str>); // key, values, param_name +type SymbolArg<'a, 'b> = (&'b str, &'a SymbolParam); type LevelCommand<'a, 'b> = (&'b str, &'a Command, usize); // name, command, arg_index #[derive(Debug, Clone, PartialEq, Eq)] @@ -40,6 +42,7 @@ pub(crate) enum ArgComp { FlagOrOptionCombine(String), CommandOrPositional, OptionValue(String, usize), + Symbol(char), Any, } @@ -65,12 +68,13 @@ pub(crate) enum MatchError { impl<'a, 'b> Matcher<'a, 'b> { pub(crate) fn new(root: &'a Command, args: &'b [String], compgen: bool) -> Self { - let combine_shorts = root.metadata.contains_key("combine-shorts"); + let combine_shorts = root.has_metadata("combine-shorts"); let mut cmds: Vec = vec![(args[0].as_str(), root, 0)]; let mut cmd_level = 0; let mut arg_index = 1; let mut flag_option_args = vec![vec![]]; let mut positional_args = vec![]; + let mut symbol_args = vec![]; let mut dashes = None; let mut split_last_arg_at = None; let mut arg_comp = ArgComp::Any; @@ -243,6 +247,15 @@ impl<'a, 'b> Matcher<'a, 'b> { arg_index, &mut is_rest_args_positional, ); + } 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 { add_positional_arg( &mut positional_args, @@ -273,6 +286,7 @@ impl<'a, 'b> Matcher<'a, 'b> { args, flag_option_args, positional_args, + symbol_args, dashes, arg_comp, choice_fns, @@ -405,6 +419,7 @@ impl<'a, 'b> Matcher<'a, 'b> { vec![] } } + ArgComp::Symbol(ch) => comp_symbol(last_cmd, *ch), ArgComp::Any => { if self.positional_args.len() == 2 && self.positional_args[0] == "help" { return comp_subcomands(last_cmd, false); @@ -437,6 +452,11 @@ impl<'a, 'b> Matcher<'a, 'b> { let level = cmds_len - 1; let last_cmd = self.cmds[level].1; let cmd_arg_index = self.cmds[level].2; + + for (arg, (name, _)) in self.symbol_args.iter() { + output.push(ArgcValue::Single(name.to_string(), arg.to_string())); + } + for level in 0..cmds_len { let args = &self.flag_option_args[level]; let cmd = self.cmds[level].1; @@ -866,6 +886,7 @@ impl<'a, 'b> Matcher<'a, 'b> { } } +/// (value, description, nospace, color) pub(crate) type CompItem = (String, String, bool, CompColor); fn find_subcommand<'a>( @@ -882,6 +903,15 @@ fn find_subcommand<'a>( }) } +fn find_symbol<'a>(cmd: &'a Command, arg: &str) -> Option<(char, &'a SymbolParam)> { + for (ch, param) in cmd.symbols.iter() { + if arg.starts_with(*ch) { + return Some((*ch, param)); + } + } + None +} + fn add_positional_arg<'a>( positional_args: &mut Vec<&'a str>, arg: &'a str, @@ -1090,6 +1120,31 @@ fn comp_subcomands(cmd: &Command, flag: bool) -> Vec { output } +fn comp_symbol(cmd: &Command, ch: char) -> Vec { + if let Some((name, choices_fn)) = cmd.symbols.get(&ch) { + match choices_fn { + Some(choices_fn) => { + vec![( + format!("__argc_fn={}", choices_fn), + String::new(), + false, + CompColor::of_value(), + )] + } + None => { + vec![( + format!("__argc_value={}", name), + String::new(), + false, + CompColor::of_value(), + )] + } + } + } else { + vec![] + } +} + fn comp_flag_option(param: &FlagOptionParam, index: usize) -> Vec { let value_name = param .arg_value_names diff --git a/src/parser.rs b/src/parser.rs index 1fe8415c..285d82a0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -109,6 +109,11 @@ pub(crate) fn parse(source: &str) -> Result> { Ok(result) } +pub(crate) fn parse_symbol(input: &str) -> Option<(char, &str, Option<&str>)> { + let input = input.trim(); + parse_symbol_data(input).map(|(_, v)| v).ok() +} + fn parse_line(line: &str) -> nom::IResult<&str, Option>> { alt((map(alt((parse_tag, parse_fn)), Some), success(None)))(line) } @@ -621,6 +626,20 @@ fn parse_normal_comment(input: &str) -> nom::IResult<&str, &str> { ))(input) } +fn parse_symbol_data(input: &str) -> nom::IResult<&str, (char, &str, Option<&str>)> { + map( + terminated( + tuple(( + alt((char('@'), char('+'))), + parse_name, + opt(delimited(char('['), parse_value_fn, char(']'))), + )), + eof, + ), + |(symbol, name, choice_fn)| (symbol, name, choice_fn), + )(input) +} + fn notation_text(input: &str, balances: usize) -> nom::IResult<&str, usize> { let (i1, c1) = anychar(input)?; match c1 { @@ -971,4 +990,16 @@ mod tests { assert_token!("foo=bar", Ignore); assert_token!("#!/bin/bash", Ignore); } + + #[test] + fn test_parse_symbol() { + assert_eq!( + parse_symbol("+toolchain").unwrap(), + ('+', "toolchain", None) + ); + assert_eq!( + parse_symbol("+toolchain[`_choice_toolchain`]").unwrap(), + ('+', "toolchain", Some("_choice_toolchain")) + ); + } } diff --git a/tests/compgen.rs b/tests/compgen.rs index b504333d..bb0d6e20 100644 --- a/tests/compgen.rs +++ b/tests/compgen.rs @@ -79,6 +79,25 @@ fn shorts() { ); } +#[test] +fn symbol() { + let script = r###" +# @meta symbol +toolchain[`_choice_fn`] +# @meta symbol @file +# @option --oa + +_choice_fn() { + echo stable + echo beta + echo nightly +} +"###; + snapshot_compgen!( + script, + [vec!["prog", "+"], vec!["prog", "@"], vec!["prog", "+s"]] + ); +} + #[test] fn subcmds() { const SCRIPT: &str = r###" diff --git a/tests/snapshots/integration__compgen__symbol.snap b/tests/snapshots/integration__compgen__symbol.snap new file mode 100644 index 00000000..36a21d5d --- /dev/null +++ b/tests/snapshots/integration__compgen__symbol.snap @@ -0,0 +1,16 @@ +--- +source: tests/compgen.rs +expression: data +--- +************ COMPGEN `prog +` ************ +stable /color:default +beta /color:default +nightly /color:default + +************ COMPGEN `prog @` ************ +__argc_value=file /color:default + +************ COMPGEN `prog +s` ************ +stable /color:default + + diff --git a/tests/snapshots/integration__export__case1.snap b/tests/snapshots/integration__export__case1.snap index 7f0a1618..70cbf368 100644 --- a/tests/snapshots/integration__export__case1.snap +++ b/tests/snapshots/integration__export__case1.snap @@ -7,9 +7,12 @@ expression: output "name": null, "author": null, "version": null, - "metadata": { - "combine-shorts": "" - }, + "metadata": [ + [ + "combine-shorts", + "" + ] + ], "options": [], "positionals": [], "aliases": [], @@ -19,7 +22,7 @@ expression: output "name": "cmda", "author": null, "version": null, - "metadata": {}, + "metadata": [], "options": [ { "name": "a", @@ -268,7 +271,7 @@ expression: output "name": "cmdb", "author": null, "version": null, - "metadata": {}, + "metadata": [], "options": [ { "name": "oa", @@ -367,7 +370,7 @@ expression: output "name": "cmdc", "author": null, "version": null, - "metadata": {}, + "metadata": [], "options": [ { "name": "oe", @@ -425,7 +428,7 @@ expression: output "name": "cmdd", "author": null, "version": null, - "metadata": {}, + "metadata": [], "options": [ { "name": "oa", @@ -451,7 +454,7 @@ expression: output "name": "cmde", "author": null, "version": null, - "metadata": {}, + "metadata": [], "options": [ { "name": "oa", diff --git a/tests/snapshots/integration__spec__symbol.snap b/tests/snapshots/integration__spec__symbol.snap new file mode 100644 index 00000000..b86e8817 --- /dev/null +++ b/tests/snapshots/integration__spec__symbol.snap @@ -0,0 +1,14 @@ +--- +source: tests/spec.rs +expression: data +--- +************ RUN ************ +prog +nightly + +OUTPUT +argc_toolchain=nightly +argc__args=( prog +nightly ) +argc__cmd_arg_index=0 +argc__positionals=( ) + + diff --git a/tests/spec.rs b/tests/spec.rs index 306d2b74..369328de 100644 --- a/tests/spec.rs +++ b/tests/spec.rs @@ -305,3 +305,12 @@ cmdb() { ] ); } + +#[test] +fn symbol() { + let script = r###" +# @meta symbol +toolchain +# @option --oa +"###; + snapshot_multi!(script, [vec!["prog", "+nightly"]]); +}