From 9e5a2a7c95d3e5106d9a576a3136f8127a46625f Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sat, 18 Jan 2020 23:33:02 +0100
Subject: [PATCH] Add argument handling in extensions

---
 src/extension/random.rs | 33 ++++++++++++++++++--
 src/extension/script.rs | 69 +++++++++++++++++++++++++++++++++++++++--
 src/extension/shell.rs  | 54 +++++++++++++++++++++++++++++---
 3 files changed, 147 insertions(+), 9 deletions(-)

diff --git a/src/extension/random.rs b/src/extension/random.rs
index 909a696..6923a6e 100644
--- a/src/extension/random.rs
+++ b/src/extension/random.rs
@@ -34,7 +34,7 @@ impl super::Extension for RandomExtension {
         String::from("random")
     }
 
-    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
+    fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> {
         let choices = params.get(&Value::from("choices"));
         if choices.is_none() {
             warn!("No 'choices' parameter specified for random variable");
@@ -51,7 +51,10 @@ impl super::Extension for RandomExtension {
 
             match choice {
                 Some(output) => {
-                    return Some(output.clone())
+                    // Render arguments
+                    let output = crate::render::utils::render_args(output, args);
+
+                    return Some(output)
                 },
                 None => {
                     error!("Could not select a random choice.");
@@ -91,5 +94,29 @@ mod tests {
         assert!(choices.iter().any(|x| x == &output));
     }
 
-    // TODO: add test with arguments
+    #[test]
+    fn test_random_with_args() {
+        let mut params = Mapping::new();
+        let choices = vec!(
+            "first $0$",
+            "second $0$",
+            "$0$ third",
+        );
+        params.insert(Value::from("choices"), Value::from(choices.clone()));
+
+        let extension = RandomExtension::new();
+        let output = extension.calculate(&params, &vec!["test".to_owned()]);
+
+        assert!(output.is_some());
+
+        let output = output.unwrap();
+
+        let rendered_choices = vec!(
+            "first test",
+            "second test",
+            "test third",
+        );
+
+        assert!(rendered_choices.iter().any(|x| x == &output));
+    }
 }
\ No newline at end of file
diff --git a/src/extension/script.rs b/src/extension/script.rs
index ecbc1bd..554fdf5 100644
--- a/src/extension/script.rs
+++ b/src/extension/script.rs
@@ -34,7 +34,7 @@ impl super::Extension for ScriptExtension {
         String::from("script")
     }
 
-    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
+    fn calculate(&self, params: &Mapping, user_args: &Vec<String>) -> Option<String> {
         let args = params.get(&Value::from("args"));
         if args.is_none() {
             warn!("No 'args' parameter specified for script variable");
@@ -42,10 +42,17 @@ impl super::Extension for ScriptExtension {
         }
         let args = args.unwrap().as_sequence();
         if let Some(args) = args {
-            let str_args = args.iter().map(|arg| {
+            let mut str_args = args.iter().map(|arg| {
                arg.as_str().unwrap_or_default().to_string()
             }).collect::<Vec<String>>();
 
+            // The user has to enable argument concatenation explicitly
+            let inject_args = params.get(&Value::from("inject_args"))
+                .unwrap_or(&Value::from(false)).as_bool().unwrap_or(false);
+            if inject_args {
+                str_args.extend(user_args.clone());
+            }
+
             let output = if str_args.len() > 1 {
                 Command::new(&str_args[0])
                     .args(&str_args[1..])
@@ -71,4 +78,62 @@ impl super::Extension for ScriptExtension {
         error!("Could not execute script with args '{:?}'", args);
         None
     }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::extension::Extension;
+
+    #[test]
+    fn test_script_basic() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
+
+        let extension = ScriptExtension::new();
+        let output = extension.calculate(&params, &vec![]);
+
+        assert!(output.is_some());
+
+        if cfg!(target_os = "windows") {
+            assert_eq!(output.unwrap(), "hello world\r\n");
+        }else{
+            assert_eq!(output.unwrap(), "hello world\n");
+        }
+    }
+
+    #[test]
+    fn test_script_inject_args_off() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
+
+        let extension = ScriptExtension::new();
+        let output = extension.calculate(&params, &vec!["jon".to_owned()]);
+
+        assert!(output.is_some());
+
+        if cfg!(target_os = "windows") {
+            assert_eq!(output.unwrap(), "hello world\r\n");
+        }else{
+            assert_eq!(output.unwrap(), "hello world\n");
+        }
+    }
+
+    #[test]
+    fn test_script_inject_args_on() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
+        params.insert(Value::from("inject_args"), Value::from(true));
+
+        let extension = ScriptExtension::new();
+        let output = extension.calculate(&params, &vec!["jon".to_owned()]);
+
+        assert!(output.is_some());
+
+        if cfg!(target_os = "windows") {
+            assert_eq!(output.unwrap(), "hello world jon\r\n");
+        }else{
+            assert_eq!(output.unwrap(), "hello world jon\n");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/extension/shell.rs b/src/extension/shell.rs
index 9736628..f8e014a 100644
--- a/src/extension/shell.rs
+++ b/src/extension/shell.rs
@@ -20,6 +20,15 @@
 use serde_yaml::{Mapping, Value};
 use std::process::Command;
 use log::{warn, error};
+use regex::{Regex, Captures};
+
+lazy_static! {
+    static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") {
+        Regex::new("\\%(?P<pos>\\d+)").unwrap()
+    }else{
+        Regex::new("\\$(?P<pos>\\d+)").unwrap()
+    };
+}
 
 pub struct ShellExtension {}
 
@@ -34,7 +43,7 @@ impl super::Extension for ShellExtension {
         String::from("shell")
     }
 
-    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
+    fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> {
         let cmd = params.get(&Value::from("cmd"));
         if cmd.is_none() {
             warn!("No 'cmd' parameter specified for shell variable");
@@ -42,14 +51,25 @@ impl super::Extension for ShellExtension {
         }
         let cmd = cmd.unwrap().as_str().unwrap();
 
+        // Render positional parameters in args
+        let cmd = POS_ARG_REGEX.replace_all(&cmd, |caps: &Captures| {
+            let position_str  = caps.name("pos").unwrap().as_str();
+            let position = position_str.parse::<i32>().unwrap_or(-1);
+            if position >= 0 && position < args.len() as i32 {
+                args[position as usize].to_owned()
+            }else{
+                "".to_owned()
+            }
+        }).to_string();
+
         let output = if cfg!(target_os = "windows") {
             Command::new("cmd")
-                .args(&["/C", cmd])
+                .args(&["/C", &cmd])
                 .output()
         } else {
             Command::new("sh")
                 .arg("-c")
-                .arg(cmd)
+                .arg(&cmd)
                 .output()
         };
 
@@ -163,5 +183,31 @@ mod tests {
         assert_eq!(output.unwrap(), "hello world");
     }
 
-    // TODO: add tests with arguments
+    #[test]
+    #[cfg(not(target_os = "windows"))]
+    fn test_shell_args_unix() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("cmd"), Value::from("echo $0"));
+
+        let extension = ShellExtension::new();
+        let output = extension.calculate(&params, &vec!["hello".to_owned()]);
+
+        assert!(output.is_some());
+
+        assert_eq!(output.unwrap(), "hello\n");
+    }
+
+    #[test]
+    #[cfg(target_os = "windows")]
+    fn test_shell_args_windows() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("cmd"), Value::from("echo %0"));
+
+        let extension = ShellExtension::new();
+        let output = extension.calculate(&params, &vec!["hello".to_owned()]);
+
+        assert!(output.is_some());
+
+        assert_eq!(output.unwrap(), "hello\r\n");
+    }
 }
\ No newline at end of file