summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2019-04-03 20:44:12 +0200
committerCanop <cano.petrole@gmail.com>2019-04-03 20:44:12 +0200
commit9036176847606c794d6e6db210a67851f7a616c4 (patch)
tree7f41644a28de226c42ae42783948b4db7c322f37
parent3f09877a70841099775c2dd87959ec5231a2970e (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.rs16
-rw-r--r--src/cli.rs13
-rw-r--r--src/command_parsing.rs139
-rw-r--r--src/commands.rs6
-rw-r--r--src/errors.rs14
-rw-r--r--src/main.rs1
6 files changed, 166 insertions, 23 deletions
diff --git a/src/app.rs b/src/app.rs
index d9601e0..f39343c 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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());
+ }
}
}
diff --git a/src/cli.rs b/src/cli.rs
index bae64a8..5061290 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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;