diff options
author | Canop <cano.petrole@gmail.com> | 2020-01-16 17:39:48 +0100 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2020-01-16 17:39:48 +0100 |
commit | de05f4d9a75ec48b03429593e10a9329203be7c8 (patch) | |
tree | ea39fc930ee419752ffd1c95a9ff33b05ffbe9ba /src | |
parent | 3bebd0f10d892728ef1bb227ba170041c3091f59 (diff) |
commands given with `--cmd` must be separated
This removes ambiguities.
Default separator is `;`. If the semicolon is used in commands, you
may provide another separator using the environnment variable
`BROOT_CMD_SEPARATOR`
Diffstat (limited to 'src')
-rw-r--r-- | src/app.rs | 1 | ||||
-rw-r--r-- | src/browser_states.rs | 19 | ||||
-rw-r--r-- | src/browser_verbs.rs | 2 | ||||
-rw-r--r-- | src/clap.rs | 2 | ||||
-rw-r--r-- | src/command_parsing.rs | 168 | ||||
-rw-r--r-- | src/commands.rs | 103 |
6 files changed, 120 insertions, 175 deletions
@@ -213,7 +213,6 @@ impl App { // we execute them before even starting listening for events if let Some(unparsed_commands) = &con.launch_args.commands { let lifetime = TaskLifetime::unlimited(); - for arg_cmd in parse_command_sequence(unparsed_commands, con)? { cmd = self.apply_command(&mut writer, arg_cmd, &mut screen, con)?; self.do_pending_tasks(&mut writer, &cmd, &mut screen, con, lifetime.clone())?; diff --git a/src/browser_states.rs b/src/browser_states.rs index 70959dd..ff5af32 100644 --- a/src/browser_states.rs +++ b/src/browser_states.rs @@ -63,7 +63,7 @@ impl BrowserState { screen, &TaskLifetime::unlimited(), ), - Command::from(&tree.options.pattern), + Command::from_pattern(&tree.options.pattern), ) } @@ -446,16 +446,17 @@ impl AppState for BrowserState { warn!("refreshing base tree failed : {:?}", e); } // refresh the filtered tree, if any - match self.filtered_tree { - Some(ref mut tree) => { - if let Err(e) = tree.refresh(page_height) { - warn!("refreshing filtered tree failed : {:?}", e); + Command::from_pattern( + match self.filtered_tree { + Some(ref mut tree) => { + if let Err(e) = tree.refresh(page_height) { + warn!("refreshing filtered tree failed : {:?}", e); + } + &tree.options.pattern } - &tree.options.pattern + None => &self.tree.options.pattern, } - None => &self.tree.options.pattern, - } - .into() + ) } /// draw the flags at the bottom right of the screen diff --git a/src/browser_verbs.rs b/src/browser_verbs.rs index 2678796..74eb0a0 100644 --- a/src/browser_verbs.rs +++ b/src/browser_verbs.rs @@ -26,7 +26,7 @@ fn focus_path(path: PathBuf, screen: &mut Screen, tree: &Tree) -> AppStateCmdRes screen, &TaskLifetime::unlimited(), ), - Command::from(&tree.options.pattern), + Command::from_pattern(&tree.options.pattern), ) } diff --git a/src/clap.rs b/src/clap.rs index 221f711..0c2d79f 100644 --- a/src/clap.rs +++ b/src/clap.rs @@ -23,7 +23,7 @@ pub fn clap_app() -> clap::App<'static, 'static> { .short("c") .long("cmd") .takes_value(true) - .help("commands to execute (space separated, experimental)"), + .help("semicolon separated commands to execute (experimental)"), ) .arg( clap::Arg::with_name("dates") diff --git a/src/command_parsing.rs b/src/command_parsing.rs index b707d87..d314116 100644 --- a/src/command_parsing.rs +++ b/src/command_parsing.rs @@ -1,136 +1,68 @@ //! 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 +//! one or several commands into a vec of parsed commands -use crate::{ - app_context::AppContext, - commands::Command, - errors::ProgramError, - verb_store::PrefixSearchResult, +use { + crate::{ + app_context::AppContext, + commands::{ + Action, + Command, CommandParts, + }, + errors::ProgramError, + 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 - VerbName(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 += 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::VerbName(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. +/// The ';' separator is used to identify inputs unless it's +/// overriden in env variable BROOT_CMD_SEPARATOR. +/// Verbs are verified, to ensure the command sequence has +/// no unexpected holes. 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; - while let Some(first_token) = leftover.take().or_else(|| tokenizer.next()) { - let raw = match first_token { - CommandSequenceToken::VerbName(name) => { - let verb = match con.verb_store.search(&name) { + let separator = match std::env::var("BROOT_CMD_SEPARATOR") { + Ok(sep) if !sep.is_empty() => sep, + _ => String::from(";"), + }; + debug!("Splitting cmd sequence with {:?}", separator); + let mut commands = Vec::new(); + for input in sequence.split(&separator) { + // an input may be made of two parts: + // - a search pattern + // - a verb followed by its arguments + // we need to build a command for each part so + // that the search is effectively done before + // the verb invocation + let (pattern, verb_invocation) = CommandParts::split(input); + if let Some(pattern) = pattern { + debug!("adding pattern: {:?}", pattern); + commands.push(Command::from_raw(pattern, false)); + } + if let Some(verb_invocation) = verb_invocation { + debug!("adding verb_invocation: {:?}", verb_invocation); + let command = Command::from_raw(verb_invocation, true); + if let Action::VerbInvocate(invocation) = &command.action { + // we check that the verb exists to avoid running a sequence + // of actions with some missing + match con.verb_store.search(&invocation.name) { PrefixSearchResult::NoMatch => { - return Err(ProgramError::UnknownVerb { name }); - } - PrefixSearchResult::TooManyMatches(..) => { - return Err(ProgramError::AmbiguousVerbName { name }); + return Err(ProgramError::UnknownVerb{ + name: invocation.name.to_string(), + }); } - PrefixSearchResult::Match(verb) => verb, - }; - let mut raw = format!(":{}", name); - 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::VerbName(_) => { - 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 { name }); - } - for (i, arg) in args.drain(..).enumerate() { - if i < nb_valid_args { - raw.push(' '); - raw.push_str(&arg); - } else { - commands.push(Command::from(arg)); - } + PrefixSearchResult::TooManyMatches(_) => { + return Err(ProgramError::AmbiguousVerbName{ + name: invocation.name.to_string(), + }); } + _ => {} } - raw + commands.push(command); } - 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::VerbName(raw) => format!(":{}", raw), - })); + } } Ok(commands) } diff --git a/src/commands.rs b/src/commands.rs index 9c76d2d..fbf3038 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -20,10 +20,10 @@ pub struct Command { /// An intermediate parsed representation of the raw string #[derive(Debug, Clone)] -struct CommandParts { +pub struct CommandParts { pattern: Option<String>, // either a fuzzy pattern or the core of a regex regex_flags: Option<String>, // may be Some("") if user asked for a regex but specified no flag - verb_invocation: Option<VerbInvocation>, // may be empty if user already typed the separator but no char after + verb_invocation: Option<VerbInvocation>, // may be empty if user typed the separator but no char after } #[derive(Debug, Clone)] @@ -54,9 +54,9 @@ impl CommandParts { verb_invocation: None, } } - fn from(raw: &str) -> CommandParts { + fn from(raw: &str) -> Self { let mut cp = CommandParts::new(); - let r = regex!( + let c = regex!( r"(?x) ^ (?P<slash_before>/)? @@ -64,23 +64,40 @@ impl CommandParts { (?:/(?P<regex_flags>\w*))? (?:[\s:]+(?P<verb_invocation>.*))? $ - " - ); - if let Some(c) = r.captures(raw) { - if let Some(pattern) = c.name("pattern") { - cp.pattern = Some(String::from(pattern.as_str())); - if let Some(rxf) = c.name("regex_flags") { - cp.regex_flags = Some(String::from(rxf.as_str())); - } else if c.name("slash_before").is_some() { - cp.regex_flags = Some("".into()); - } - } - if let Some(verb) = c.name("verb_invocation") { - cp.verb_invocation = Some(VerbInvocation::from(verb.as_str())); + " + ) + .captures(raw) + .unwrap(); // all parts optional, so always captures + if let Some(pattern) = c.name("pattern") { + cp.pattern = Some(String::from(pattern.as_str())); + if let Some(rxf) = c.name("regex_flags") { + cp.regex_flags = Some(String::from(rxf.as_str())); + } else if c.name("slash_before").is_some() { + cp.regex_flags = Some("".into()); } } + if let Some(verb) = c.name("verb_invocation") { + cp.verb_invocation = Some(VerbInvocation::from(verb.as_str())); + } cp } + /// split an input into its two possible parts, the pattern + /// and the verb invocation. Each part, when defined, is + /// suitable to create a command on its own. + pub fn split(raw: &str) -> (Option<String>, Option<String>) { + let captures = regex!( + r"(?x) + ^ + (?P<pattern_part>/?[^\s/:]+/?\w*)? + (?P<verb_part>[\s:]+(.+))? + $ + " + ).captures(raw).unwrap(); // all parts optional : always captures + ( + captures.name("pattern_part").map(|c| c.as_str().to_string()), + captures.name("verb_part").map(|c| c.as_str().to_string()), + ) + } } impl Default for CommandParts { @@ -121,6 +138,30 @@ impl Command { } } + /// create a command from a raw input. `finished` makes + /// the command an executed form, it's equivalent to + /// using the Enter key in the Gui. + pub fn from_raw(raw: String, finished: bool) -> Self { + let parts = CommandParts::from(&raw); + Self { + raw, + action: Action::from(&parts, finished), + parts, + } + } + + /// build a non executed command from a pattern + pub fn from_pattern(pattern: &Pattern) -> Self { + Command::from_raw( + match pattern { + Pattern::Fuzzy(fp) => fp.to_string(), + Pattern::Regex(rp) => rp.to_string(), + Pattern::None => String::new(), + }, + false, + ) + } + /// set the action and clears the other parts : /// the command is now just the action. /// This isn't used when the parts must be kept, @@ -230,34 +271,6 @@ impl Command { } } -impl From<String> for Command { - /// 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 - /// of a file or from a sequence of commands passed as argument - /// of the program. - /// A ':', even if at the end, is assumed to mean that the - /// command must be executed (it's equivalent to the user - /// typing `enter` in the app - /// This specific syntax isn't definitive - fn from(raw: String) -> Command { - let parts = CommandParts::from(&raw); - let action = Action::from(&parts, raw.contains(':')); - Command { raw, parts, action } - } -} - -impl From<&Pattern> for Command { - fn from(pattern: &Pattern) -> Self { - match pattern { - Pattern::Fuzzy(fp) => fp.to_string(), - Pattern::Regex(rp) => rp.to_string(), - Pattern::None => String::new(), - } - .into() - } -} - impl Default for Command { fn default() -> Command { Command::new() |