summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2020-01-16 17:39:48 +0100
committerCanop <cano.petrole@gmail.com>2020-01-16 17:39:48 +0100
commitde05f4d9a75ec48b03429593e10a9329203be7c8 (patch)
treeea39fc930ee419752ffd1c95a9ff33b05ffbe9ba /src
parent3bebd0f10d892728ef1bb227ba170041c3091f59 (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.rs1
-rw-r--r--src/browser_states.rs19
-rw-r--r--src/browser_verbs.rs2
-rw-r--r--src/clap.rs2
-rw-r--r--src/command_parsing.rs168
-rw-r--r--src/commands.rs103
6 files changed, 120 insertions, 175 deletions
diff --git a/src/app.rs b/src/app.rs
index b6eaec3..6ab3068 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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()