commit cdb7201767eacf348708e48d7142e18f2aa85924
parent 88369a11c73c5b654c83fe489159908aa3b406b5
Author: Stefan Koch <programming@stefan-koch.name>
Date: Tue, 24 Sep 2019 22:42:28 +0200
create a command parser that can handle quoted arguments
Diffstat:
3 files changed, 90 insertions(+), 10 deletions(-)
diff --git a/src/execution.rs b/src/execution.rs
@@ -4,6 +4,7 @@ use std::io::Write;
use evalexpr;
+use crate::parser;
use crate::pipeline;
pub enum ExecutionResult {
@@ -63,8 +64,8 @@ fn execute_pipeline<W: Write>(
let cmd = replace_variables(&cmd, &variables);
// TODO: Raise error if some variables remain unsubstituted?
- let parts = split_command(&cmd);
- let output = Command::new(parts[0])
+ let parts = parser::parse_command(&cmd);
+ let output = Command::new(&parts[0])
.args(&parts[1..])
.output();
let output = match output {
@@ -87,14 +88,6 @@ fn execute_pipeline<W: Write>(
ExecutionResult::Success
}
-fn split_command<'a>(command: &'a str) -> Vec<&'a str> {
- // TODO: Successful argument parsing needs a lot more details,
- // e.g. for quoted arguments like myprogram "argument 1"
- // but for a first shot this works
- let parts: Vec<&str> = command.split(" ").collect();
- parts
-}
-
fn execute_test(test: &str, variables: &HashMap<String, String>) -> bool {
// not possible to use evalexpr Context, because evalexpr only handles
// standard variable names without special characters (percentage
diff --git a/src/lib.rs b/src/lib.rs
@@ -8,6 +8,7 @@ use rand::distributions::Alphanumeric;
mod config;
mod vcs;
+mod parser;
mod pipeline;
mod execution;
mod mail;
diff --git a/src/parser.rs b/src/parser.rs
@@ -0,0 +1,86 @@
+pub fn parse_command(command: &str) -> Vec<String> {
+ // TODO: improve this to a good state machine
+ let mut parts = Vec::new();
+ let mut in_quotes = false;
+ let mut in_word = false;
+ let mut start_idx = 0;
+ let mut next_char_escaped = false;
+ let mut buffer = String::new();
+
+ for (i, c) in command.chars().enumerate() {
+ if in_quotes {
+ if c == '"' {
+ parts.push(String::from(&command[start_idx..i]));
+ in_quotes = false;
+ }
+ } else {
+ if c == '\\' && !next_char_escaped {
+ buffer.push_str(&command[start_idx..i]);
+ buffer.push_str(&command[(i+1)..(i+2)]);
+ next_char_escaped = true;
+ start_idx = i + 2;
+ } else if c == '"' && !next_char_escaped {
+ in_quotes = true;
+ start_idx = i + 1;
+ next_char_escaped = false;
+ } else if !next_char_escaped && c != ' ' && !in_word {
+ start_idx = i;
+ in_word = true;
+ next_char_escaped = false;
+ } else if !next_char_escaped && c == ' ' && in_word {
+ parts.push(format!("{}{}", buffer, String::from(&command[start_idx..i])));
+ buffer = String::new();
+ in_word = false;
+ next_char_escaped = false;
+ }
+ }
+ }
+
+ let end = command.len();
+ if start_idx != end {
+ parts.push(format!("{}{}", buffer, String::from(&command[start_idx..end])));
+ }
+
+ parts
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_simple_command() {
+ let result = parse_command("program execute something");
+
+ assert_eq!(result[0], "program");
+ assert_eq!(result[1], "execute");
+ assert_eq!(result[2], "something");
+ }
+
+ #[test]
+ fn test_parse_command_with_quoted_args() {
+ let result = parse_command("program \"execute something\" and \"something else\"");
+
+ assert_eq!(result[0], "program");
+ assert_eq!(result[1], "execute something");
+ assert_eq!(result[2], "and");
+ assert_eq!(result[3], "something else");
+ }
+
+ #[test]
+ fn test_parse_command_with_spaced_arg() {
+ let result = parse_command("program execute\\ something");
+
+ assert_eq!(result[0], "program");
+ assert_eq!(result[1], "execute something");
+ }
+
+ #[test]
+ fn test_parse_virtualenv_tox_command() {
+ let result = parse_command("bash -c \"virtualenv env && source env/bin/activate && tox\"");
+
+ assert_eq!(result[0], "bash");
+ assert_eq!(result[1], "-c");
+ assert_eq!(result[2], "virtualenv env && source env/bin/activate && tox");
+ }
+}