diff options
author | Canop <cano.petrole@gmail.com> | 2019-04-03 20:44:12 +0200 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2019-04-03 20:44:12 +0200 |
commit | 9036176847606c794d6e6db210a67851f7a616c4 (patch) | |
tree | 7f41644a28de226c42ae42783948b4db7c322f37 | |
parent | 3f09877a70841099775c2dd87959ec5231a2970e (diff) |
try to give arguments to verbs executed through --cmd
Parsing the --cmd argument is now done using the matching verb,
and tries to give as many token as allowed by the execution pattern
of the verb.
If the --cmd argument seems non consistent, an error is thrown
and nothing is executed
Fix #49
-rw-r--r-- | src/app.rs | 16 | ||||
-rw-r--r-- | src/cli.rs | 13 | ||||
-rw-r--r-- | src/command_parsing.rs | 139 | ||||
-rw-r--r-- | src/commands.rs | 6 | ||||
-rw-r--r-- | src/errors.rs | 14 | ||||
-rw-r--r-- | src/main.rs | 1 |
6 files changed, 166 insertions, 23 deletions
@@ -17,6 +17,7 @@ use termion::input::TermRead; use crate::app_context::AppContext; use crate::browser_states::BrowserState; use crate::commands::Command; +use crate::command_parsing::parse_command_sequence; use crate::errors::ProgramError; use crate::errors::TreeBuildError; use crate::external::Launchable; @@ -224,12 +225,15 @@ impl App { // if some commands were passed to the application // we execute them before even starting listening for keys - for arg_cmd in &con.launch_args.commands { - cmd = (*arg_cmd).clone(); - cmd = self.apply_command(cmd, &mut screen, con)?; - self.do_pending_tasks(&cmd, &mut screen, con, TaskLifetime::unlimited())?; - if self.quitting { - return Ok(self.launch_at_end.take()); + if let Some(unparsed_commands) = &con.launch_args.commands { + let commands = parse_command_sequence(unparsed_commands, con)?; + for arg_cmd in &commands { + cmd = (*arg_cmd).clone(); + cmd = self.apply_command(cmd, &mut screen, con)?; + self.do_pending_tasks(&cmd, &mut screen, con, TaskLifetime::unlimited())?; + if self.quitting { + return Ok(self.launch_at_end.take()); + } } } @@ -1,7 +1,6 @@ /// this module manages reading and translating /// the arguments passed on launch of the application. -use crate::commands::Command; use crate::errors::{ProgramError, TreeBuildError}; use crate::tree_options::TreeOptions; use clap; @@ -16,7 +15,7 @@ pub struct AppLaunchArgs { pub file_export_path: Option<String>, // where to write the produced path (if required with --out) pub cmd_export_path: Option<String>, // where to write the produced command (if required with --outcmd, or -oc) pub tree_options: TreeOptions, // initial tree options - pub commands: Vec<Command>, // commands passed as cli argument + pub commands: Option<String>, // commands passed as cli argument, still unparsed pub install: bool, // installation is required pub height: Option<u16>, // an optional height to replace the screen's one pub no_style: bool, // whether to remove all styles (including colors) @@ -139,13 +138,9 @@ pub fn read_lauch_args() -> Result<AppLaunchArgs, ProgramError> { let cmd_export_path = cli_args .value_of("cmd_export_path") .and_then(|s| Some(s.to_owned())); - let commands: Vec<Command> = match cli_args.value_of("commands") { - Some(str) => str - .split(' ') - .map(|s| Command::from(s.to_string())) - .collect(), - None => Vec::new(), - }; + let commands = cli_args + .value_of("commands") + .and_then(|s| Some(s.to_owned())); let no_style = cli_args.is_present("no-style"); let height = cli_args .value_of("height") diff --git a/src/command_parsing.rs b/src/command_parsing.rs new file mode 100644 index 0000000..280442a --- /dev/null +++ b/src/command_parsing.rs @@ -0,0 +1,139 @@ +//! this mod achieves the transformation of a string containing +//! one or several commands into a vec of parsed commands, using +//! the verbstore to try guess what part is an argument and what +//! part is a filter + +use crate::app_context::AppContext; +use crate::commands::Command; +use crate::errors::ProgramError; +use crate::verb_store::PrefixSearchResult; + +#[derive(Debug)] +enum CommandSequenceToken { + Standard(String), // one or several words, not starting with a ':'. May be a filter or a verb argument + VerbKey(String), // a verb (the ':' isn't given) +} +struct CommandSequenceTokenizer { + chars: Vec<char>, + pos: usize, +} +impl CommandSequenceTokenizer { + pub fn from(sequence: &str) -> CommandSequenceTokenizer { + CommandSequenceTokenizer { + chars: sequence.chars().collect(), + pos: 0 + } + } +} +impl Iterator for CommandSequenceTokenizer { + type Item = CommandSequenceToken; + fn next(&mut self) -> Option<CommandSequenceToken> { + if self.pos >= self.chars.len() { + return None; + } + let is_verb = if self.chars[self.pos] == ':' { + self.pos = self.pos + 1; + true + } else { + false + }; + let mut end = self.pos; + let mut between_quotes = false; + while end < self.chars.len() { + if self.chars[end] == '"' { + between_quotes = !between_quotes; + } else if self.chars[end] == ' ' && !between_quotes { + break; + } + end += 1; + } + let token: String = self.chars[self.pos..end].iter().collect(); + self.pos = end + 1; + Some( + if is_verb { + CommandSequenceToken::VerbKey(token) + } else { + CommandSequenceToken::Standard(token) + } + ) + } +} + +/// parse a string which is meant as a sequence of commands. +/// Note that this is inherently flawed as packing several commands +/// into a string without hard separator is ambiguous in the general +/// case. +/// +/// In the future I might introduce a way to define a variable hard separator +/// (for example "::sep=#:some_filter#:some command with three arguments#a_filter") +/// +/// The current parsing try to be the least possible flawed by +/// giving verbs the biggest sequence of tokens accepted by their +/// execution pattern. +pub fn parse_command_sequence(sequence: &str, con: &AppContext) -> Result<Vec<Command>, ProgramError> { + let mut tokenizer = CommandSequenceTokenizer::from(sequence); + let mut commands: Vec<Command> = Vec::new(); + let mut leftover: Option<CommandSequenceToken> = None; + loop { + let first_token = if let Some(token) = leftover.take().or_else(|| tokenizer.next()) { + token + } else { + break; + }; + let raw = match first_token { + CommandSequenceToken::VerbKey(key) => { + let verb = match con.verb_store.search(&key) { + PrefixSearchResult::NoMatch => { + return Err(ProgramError::UnknownVerb{key}); + } + PrefixSearchResult::TooManyMatches => { + return Err(ProgramError::AmbiguousVerbKey{key}); + } + PrefixSearchResult::Match(verb) => verb + }; + let mut raw = format!(":{}", key); + if let Some(args_regex) = &verb.args_parser { + let mut args: Vec<String> = Vec::new(); + let mut nb_valid_args = 0; + // we'll try to consume as many tokens as possible + while let Some(token) = tokenizer.next() { + match token { + CommandSequenceToken::VerbKey(_) => { + leftover = Some(token); + break; + } + CommandSequenceToken::Standard(raw) => { + args.push(raw); + if args_regex.is_match(&args.join(" ")) { + nb_valid_args = args.len(); + } + } + } + } + if nb_valid_args == 0 && !args_regex.is_match("") { + return Err(ProgramError::UnmatchingVerbArgs{key}); + } + for (i, arg) in args.drain(..).enumerate() { + if i < nb_valid_args { + raw.push(' '); + raw.push_str(&arg); + } else { + commands.push(Command::from(arg)); + } + } + } + raw + } + CommandSequenceToken::Standard(raw) => raw + }; + commands.push(Command::from(raw)); + } + if let Some(token) = leftover.take() { + commands.push(Command::from(match token{ + CommandSequenceToken::Standard(raw) => raw, + CommandSequenceToken::VerbKey(raw) => format!(":{}", raw), + })); + } + Ok(commands) +} + diff --git a/src/commands.rs b/src/commands.rs index 7008d00..0b7a256 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -27,8 +27,8 @@ pub enum Action { ScrollPage(i32), // in number of pages, not lines OpenSelection, // open the selected line AltOpenSelection, // alternate open the selected line - VerbEdit(VerbInvocation), // verb invocation, unfinished - Verb(VerbInvocation), // verb invocation, after the user hit enter + VerbEdit(VerbInvocation), // verb invocation, unfinished + Verb(VerbInvocation), // verb invocation, after the user hit enter FuzzyPatternEdit(String), // a pattern being edited RegexEdit(String, String), // a regex being edited (core & flags) Back, // back to last app state, or clear pattern @@ -110,6 +110,7 @@ impl Command { action: Action::Unparsed, } } + // build a command from a string // Note that this isn't used (or usable) for interpretation // of the in-app user input. It's meant for interpretation @@ -124,6 +125,7 @@ impl Command { let action = Action::from(&parts, raw.contains(':')); Command { raw, parts, action } } + pub fn add_key(&mut self, key: Key) { match key { Key::Char('\t') => { diff --git a/src/errors.rs b/src/errors.rs index fde7b27..9f24d71 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -13,6 +13,9 @@ custom_error! {pub ProgramError Io {source: io::Error} = "IO Error : {:?}", Conf {source: ConfError} = "Bad configuration: {}", ArgParse {bad: String, valid: String} = "{:?} can't be parsed (valid values: {:?})", + UnknownVerb {key: String} = "No verb matches {:?}", + AmbiguousVerbKey {key: String} = "Ambiguous key: More than one verb matches {:?}", + UnmatchingVerbArgs {key: String} = "No matching argument found for verb {:?}", TreeBuild {source: TreeBuildError} = "{}", OpenError {err: opener::OpenError} = "{}", LaunchError {program: String, source: io::Error} = "Unable to launch {program}: {source}", @@ -32,10 +35,9 @@ custom_error! {pub InvalidSkinError } custom_error! {pub ConfError - Io{source: io::Error} = "unable to read from the file", - Toml{source: toml::de::Error} = "unable to parse TOML", - MissingField{txt: String} = "missing field in conf", - InvalidSkinEntry{ - key:String, source: InvalidSkinError} = "Invalid skin configuration for {}: {}", - InvalidVerbInvocation{invocation: String} = "invalid verb invocation: {}", + Io {source: io::Error} = "unable to read from the file", + Toml {source: toml::de::Error} = "unable to parse TOML", + MissingField {txt: String} = "missing field in conf", + InvalidSkinEntry { key:String, source: InvalidSkinError} = "Invalid skin configuration for {}: {}", + InvalidVerbInvocation {invocation: String} = "invalid verb invocation: {}", } diff --git a/src/main.rs b/src/main.rs index 9e9963c..480a9a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod browser_states; mod browser_verbs; mod cli; mod commands; +mod command_parsing; mod conf; mod errors; mod external; |