diff --git a/src/parser/shell_expand/mod.rs b/src/parser/shell_expand/mod.rs index 070677d325922e5bdbc7f88bbba42cdbee7ae649..9ffee510d90ae244dd607b1c4e5fa466c4fe37de 100644 --- a/src/parser/shell_expand/mod.rs +++ b/src/parser/shell_expand/mod.rs @@ -587,6 +587,14 @@ mod test { } } + #[test] + fn expand_process_quoted() { + let mut output = String::new(); + let line = " Mary had\ta little \n\t lamb\t"; + expand_process(&mut output, line, Select::All, &CommandExpander, true); + assert_eq!(output, line); + } + #[test] fn expand_process_unquoted() { let mut output = String::new(); diff --git a/src/parser/shell_expand/words/methods/strings.rs b/src/parser/shell_expand/words/methods/strings.rs index af54dd52707845917819e531d51fc6cdb92662da..f82e4140e76c093512da2b75c6bb731f71047675 100644 --- a/src/parser/shell_expand/words/methods/strings.rs +++ b/src/parser/shell_expand/words/methods/strings.rs @@ -12,17 +12,80 @@ lazy_static! { static ref STRING_METHODS: StringMethodPlugins = methods::collect(); } +fn unescape(input: &str) -> Result<String, &'static str> { + let mut check = false; + let mut out = String::with_capacity(input.len()); + let add_char = |out: &mut String, check: &mut bool, c| { + out.push(c); + *check = false; + }; + for c in input.chars() { + match c { + '\\' if check => { + add_char(&mut out, &mut check, c); + } + '\\' => check = true, + '\'' if check => add_char(&mut out, &mut check, c), + '\"' if check => add_char(&mut out, &mut check, c), + 'a' if check => add_char(&mut out, &mut check, '\u{0007}'), + 'b' if check => add_char(&mut out, &mut check, '\u{0008}'), + 'c' if check => { + out = String::from(""); + break; + }, + 'e' if check => add_char(&mut out, &mut check, '\u{001B}'), + 'f' if check => add_char(&mut out, &mut check, '\u{000C}'), + 'n' if check => add_char(&mut out, &mut check, '\n'), + 'r' if check => add_char(&mut out, &mut check, '\r'), + 't' if check => add_char(&mut out, &mut check, '\t'), + 'v' if check => add_char(&mut out, &mut check, '\u{000B}'), + _ if check => { + out.push('\\'); + add_char(&mut out, &mut check, c); + } + c if c.is_ascii() => out.push(c), + _ => return Err("ion: Invalid ASCII character"), + } + } + Ok(out) +} +fn escape(input: &str) -> Result<String, &'static str> { + let mut output = String::with_capacity(input.len() * 2); + for b in input.as_bytes() { + match *b { + 0 => output.push_str("\\0"), + 7 => output.push_str("\\a"), + 8 => output.push_str("\\b"), + 9 => output.push_str("\\t"), + 10 => output.push_str("\\n"), + 11 => output.push_str("\\v"), + 12 => output.push_str("\\f"), + 13 => output.push_str("\\r"), + 27 => output.push_str("\\e"), + n if n != 59 && n != 95 && + ((n >= 33 && n < 48) || + (n >= 58 && n < 65) || + (n >= 91 && n < 97) || + (n >= 123 && n < 127)) => { + output.push('\\'); + output.push(n as char); + }, + n if n <= 127 => output.push(n as char), + _ => return Err("ion: Invalid ASCII character"), + } + } + Ok(output) +} + /// Represents a method that operates on and returns a string #[derive(Debug, PartialEq, Clone)] pub(crate) struct StringMethod<'a> { - /// Name of this method: currently `join`, `len`, and `len_bytes` are the - /// supported methods + /// Name of this method pub(crate) method: &'a str, /// Variable that this method will operator on. This is a bit of a misnomer /// as this can be an expression as well pub(crate) variable: &'a str, - /// Pattern to use for certain methods: currently `join` makes use of a - /// pattern + /// Pattern to use for certain methods pub(crate) pattern: &'a str, /// Selection to use to control the output of this method pub(crate) selection: Select, @@ -94,7 +157,7 @@ impl<'a> StringMethod<'a> { "repeat" => match pattern.join(" ").parse::<usize>() { Ok(repeat) => output.push_str(&get_var!().repeat(repeat)), Err(_) => { - eprintln!("ion: value supplied to $repeat() is not a valid number"); + eprintln!("ion: value supplied to $repeat() is not a valid positive integer"); } }, "replace" => { @@ -182,107 +245,22 @@ impl<'a> StringMethod<'a> { } else { None }; - output.push_str(&out.unwrap_or(0).to_string()); + output.push_str(&out.map(|i| i as isize).unwrap_or(-1).to_string()); }, "unescape" => { - fn unescape(input: String) -> String { - let mut check = false; - let mut out = String::with_capacity(input.len()); - for c in input.chars() { - match c { - '\\' if check => { - out.push(c); - check = false; - } - '\\' => check = true, - '\'' if check => { - out.push(c); - check = false; - } - '\"' if check => { - out.push(c); - check = false; - } - 'a' if check => { - out.push('\u{0007}'); - check = false; - } - 'b' if check => { - out.push('\u{0008}'); - check = false; - } - 'c' if check => { - out = String::from(""); - break; - } - 'e' if check => { - out.push('\u{001B}'); - check = false; - } - 'f' if check => { - out.push('\u{000C}'); - check = false; - } - 'n' if check => { - out.push('\n'); - check = false; - } - 'r' if check => { - out.push('\r'); - check = false; - } - 't' if check => { - out.push('\t'); - check = false; - } - 'v' if check => { - out.push('\u{000B}'); - check = false; - } - _ if check => { - out.push('\\'); - out.push(c); - check = false; - } - _ => { out.push(c); } - } - } - out - } - if let Some(value) = expand.variable(variable, false) { - output.push_str(&unescape(value)); + let out = if let Some(value) = expand.variable(variable, false) { + value } else if is_expression(variable) { - output.push_str(&unescape(expand_string(variable, expand, false).join(" "))); + expand_string(variable, expand, false).join(" ") + } else { + return; + }; + match unescape(&out) { + Ok(out) => output.push_str(&out), + Err(msg) => eprintln!("{}", &msg) }; }, "escape" => { - fn escape(input: &str) -> Result<String, &'static str> { - let mut output = String::with_capacity(input.len() * 2); - for b in input.as_bytes() { - match *b { - 0 => output.push_str("\\0"), - 7 => output.push_str("\\a"), - 8 => output.push_str("\\b"), - 9 => output.push_str("\\t"), - 10 => output.push_str("\\n"), - 11 => output.push_str("\\v"), - 12 => output.push_str("\\f"), - 13 => output.push_str("\\r"), - 27 => output.push_str("\\e"), - n if n != 59 && n != 95 && - ((n >= 33 && n < 48) || - (n >= 58 && n < 65) || - (n >= 91 && n < 97) || - (n >= 123 && n < 127)) => { - output.push('\\'); - output.push(n as char); - }, - n if n <= 127 => output.push(n as char), - _ => return Err("ion: Invalid ASCII character"), - } - } - Ok(output) - } let word = if let Some(value) = expand.variable(variable, false) { value } else if is_expression(variable) { @@ -329,3 +307,411 @@ impl<'a> StringMethod<'a> { } } } + +#[cfg(test)] +mod test { + use super::*; + use types::Value; + + struct VariableExpander; + + impl Expander for VariableExpander { + fn variable(&self, variable: &str, _: bool) -> Option<Value> { + match variable { + "FOO" => Some("FOOBAR".to_owned()), + _ => None, + } + } + } + + #[test] + fn test_escape() { + let line = " Mary had\ta little \n\t lamb\t"; + let output = escape(line).expect("error processing string"); + assert_eq!(output, " Mary had\\ta little \\n\\t lamb\\t"); + } + + #[test] + fn test_unescape() { + let line = " Mary had\ta little \n\t lamb\t"; + let output = unescape(line).expect("error processing string"); + assert_eq!(output, line); + } + + #[test] + fn test_ends_with_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "ends_with", + variable: "$FOO", + pattern: "\"BAR\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "1"); + } + + #[test] + fn test_ends_with_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "ends_with", + variable: "$FOO", + pattern: "\"BA\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "0"); + } + + #[test] + fn test_contains_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "contains", + variable: "$FOO", + pattern: "\"OBA\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "1"); + } + + #[test] + fn test_contains_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "contains", + variable: "$FOO", + pattern: "\"OBI\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "0"); + } + + #[test] + fn test_starts_with_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "starts_with", + variable: "$FOO", + pattern: "\"FOO\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "1"); + } + + #[test] + fn test_starts_with_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "starts_with", + variable: "$FOO", + pattern: "\"OO\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "0"); + } + + #[test] + fn test_basename() { + let mut output = String::new(); + let method = StringMethod { + method: "basename", + variable: "\"/home/redox/file.txt\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "file.txt"); + } + + #[test] + fn test_extension() { + let mut output = String::new(); + let method = StringMethod { + method: "extension", + variable: "\"/home/redox/file.txt\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "txt"); + } + + #[test] + fn test_filename() { + let mut output = String::new(); + let method = StringMethod { + method: "filename", + variable: "\"/home/redox/file.txt\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "file"); + } + + #[test] + fn test_parent() { + let mut output = String::new(); + let method = StringMethod { + method: "parent", + variable: "\"/home/redox/file.txt\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "/home/redox"); + } + + #[test] + fn test_to_lowercase() { + let mut output = String::new(); + let method = StringMethod { + method: "to_lowercase", + variable: "\"Ford Prefect\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "ford prefect"); + } + + #[test] + fn test_to_uppercase() { + let mut output = String::new(); + let method = StringMethod { + method: "to_uppercase", + variable: "\"Ford Prefect\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FORD PREFECT"); + } + + #[test] + fn test_repeat_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "repeat", + variable: "$FOO", + pattern: "2", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FOOBARFOOBAR"); + } + + #[test] + fn test_repeat_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "repeat", + variable: "$FOO", + pattern: "-2", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, ""); + } + + #[test] + fn test_replace_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "replace", + variable: "$FOO", + pattern: "[\"FOO\" \"BAR\"]", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "BARBAR"); + } + + #[test] + fn test_replace_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "replace", + variable: "$FOO", + pattern: "[]", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, ""); + } + + #[test] + fn test_replacen_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "replacen", + variable: "\"FOO$FOO\"", + pattern: "[\"FOO\" \"BAR\" 1]", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "BARFOOBAR"); + } + + #[test] + fn test_replacen_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "replacen", + variable: "$FOO", + pattern: "[]", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, ""); + } + + #[test] + fn test_join_with_string() { + let mut output = String::new(); + let method = StringMethod { + method: "join", + variable: "[\"FOO\" \"BAR\"]", + pattern: "\" \"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FOO BAR"); + } + + #[test] + fn test_join_with_array() { + let mut output = String::new(); + let method = StringMethod { + method: "join", + variable: "[\"FOO\" \"BAR\"]", + pattern: "[\"-\" \"-\"]", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FOO- -BAR"); + } + + #[test] + fn test_len_with_array() { + let mut output = String::new(); + let method = StringMethod { + method: "len", + variable: "[\"1\"]", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "1"); + } + + #[test] + fn test_len_with_string() { + let mut output = String::new(); + let method = StringMethod { + method: "len", + variable: "\"FOO\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "3"); + } + + #[test] + fn test_len_with_variable() { + let mut output = String::new(); + let method = StringMethod { + method: "len", + variable: "$FOO", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "6"); + } + + #[test] + fn test_len_bytes_with_variable() { + let mut output = String::new(); + let method = StringMethod { + method: "len_bytes", + variable: "$FOO", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "6"); + } + + #[test] + fn test_len_bytes_with_string() { + let mut output = String::new(); + let method = StringMethod { + method: "len_bytes", + variable: "\"oh là là \"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "10"); + } + + #[test] + fn test_reverse_with_variable() { + let mut output = String::new(); + let method = StringMethod { + method: "reverse", + variable: "$FOO", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "RABOOF"); + } + + #[test] + fn test_reverse_with_string() { + let mut output = String::new(); + let method = StringMethod { + method: "reverse", + variable: "\"FOOBAR\"", + pattern: "", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "RABOOF"); + } + + #[test] + fn test_find_succeeding() { + let mut output = String::new(); + let method = StringMethod { + method: "find", + variable: "$FOO", + pattern: "\"O\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "1"); + } + + #[test] + fn test_find_failing() { + let mut output = String::new(); + let method = StringMethod { + method: "find", + variable: "$FOO", + pattern: "\"L\"", + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "-1"); + } +}