diff --git a/examples/array_methods.ion b/examples/array_methods.ion new file mode 100644 index 0000000000000000000000000000000000000000..5d2b36458d35d019be1f63ec9ad5628716226aeb --- /dev/null +++ b/examples/array_methods.ion @@ -0,0 +1,5 @@ +echo @split("onetwoone", "two") +echo @split_at("onetwoone", "3") +echo @graphemes("onetwo", "3") +echo @bytes("onetwo") +echo @chars("onetwo") diff --git a/examples/array_methods.out b/examples/array_methods.out new file mode 100644 index 0000000000000000000000000000000000000000..e692ef7b395af9c5c458eac7fb361187fff89ac2 --- /dev/null +++ b/examples/array_methods.out @@ -0,0 +1,5 @@ +one one +one twoone +o n e t w o +111 110 101 116 119 111 +o n e t w o diff --git a/examples/run_examples.sh b/examples/run_examples.sh index a10e9f17e5611c84537c05ebd4945042804a07eb..2a644419ff81a074d88e6deb981ca85a20370029 100755 --- a/examples/run_examples.sh +++ b/examples/run_examples.sh @@ -32,7 +32,7 @@ function check_return_value { $PROJECT_DIR/target/debug/ion $1 1> $EXAMPLES_DIR/tmp.out 2> /dev/null # Compare real and expected output - cmp --silent $EXAMPLES_DIR/tmp.out $EXPECTED_OUTPUT_FILE + diff "$EXAMPLES_DIR"/tmp.out "$EXPECTED_OUTPUT_FILE" > "$EXAMPLES_DIR"/diff_tmp local RET=$? # Clean up the mess @@ -40,9 +40,12 @@ function check_return_value { # Write result if [[ $RET -ne 0 ]]; then + cat "$EXAMPLES_DIR"/diff_tmp + rm "$EXAMPLES_DIR"/diff_tmp echo -e "Test ${1} ${TAGFAIL}"; return 1; else + rm "$EXAMPLES_DIR"/diff_tmp echo -e "Test ${1} ${TAGPASS}"; return 0; fi diff --git a/src/parser/shell_expand/words/methods/arrays.rs b/src/parser/shell_expand/words/methods/arrays.rs index 002dabbcfeb6c93120440d8063a5fbd4570d6f03..3c68c5adf44457209b2d388ebc1c1fd316419a1e 100644 --- a/src/parser/shell_expand/words/methods/arrays.rs +++ b/src/parser/shell_expand/words/methods/arrays.rs @@ -4,7 +4,6 @@ use super::super::{Index, Select, SelectWithSize}; use super::super::super::{expand_string, is_expression, Expander}; use smallstring::SmallString; use std::char; -use std::io::{self, Write}; use types::Array; use unicode_segmentation::UnicodeSegmentation; @@ -18,223 +17,382 @@ pub(crate) struct ArrayMethod<'a> { impl<'a> ArrayMethod<'a> { pub(crate) fn handle<E: Expander>(&self, current: &mut String, expand_func: &E) { - match self.method { - "split" => { - let variable = if let Some(variable) = expand_func.variable(self.variable, false) { - variable - } else if is_expression(self.variable) { - expand_string(self.variable, expand_func, false).join(" ") - } else { - return; - }; - match (&self.pattern, self.selection.clone()) { - (&Pattern::StringPattern(pattern), Select::All) => current.push_str(&variable - .split(&unescape(expand_string(pattern, expand_func, false).join(" "))) - .collect::<Vec<&str>>() - .join(" ")), - (&Pattern::Whitespace, Select::All) => current.push_str(&variable - .split(char::is_whitespace) - .filter(|x| !x.is_empty()) - .collect::<Vec<&str>>() - .join(" ")), - (_, Select::None) => (), - (&Pattern::StringPattern(pattern), Select::Index(Index::Forward(id))) => { - current.push_str( - variable - .split( - &unescape(expand_string(pattern, expand_func, false).join(" ")), - ) - .nth(id) - .unwrap_or_default(), - ) - } - (&Pattern::Whitespace, Select::Index(Index::Forward(id))) => current.push_str( - variable - .split(char::is_whitespace) - .filter(|x| !x.is_empty()) - .nth(id) - .unwrap_or_default(), - ), - (&Pattern::StringPattern(pattern), Select::Index(Index::Backward(id))) => { - current.push_str( - variable - .rsplit( - &unescape(expand_string(pattern, expand_func, false).join(" ")), - ) - .nth(id) - .unwrap_or_default(), - ) - } - (&Pattern::Whitespace, Select::Index(Index::Backward(id))) => current - .push_str( - variable - .rsplit(char::is_whitespace) - .filter(|x| !x.is_empty()) - .nth(id) - .unwrap_or_default(), - ), - (&Pattern::StringPattern(pattern), Select::Range(range)) => { - let expansion = unescape( - unescape(expand_string(pattern, expand_func, false).join(" ")), - ); - let iter = variable.split(&expansion); - if let Some((start, length)) = range.bounds(iter.clone().count()) { - let range = iter.skip(start).take(length).collect::<Vec<_>>().join(" "); - current.push_str(&range) - } - } - (&Pattern::Whitespace, Select::Range(range)) => { - let len = - variable.split(char::is_whitespace).filter(|x| !x.is_empty()).count(); - if let Some((start, length)) = range.bounds(len) { - let range = variable - .split(char::is_whitespace) - .filter(|x| !x.is_empty()) - .skip(start) - .take(length) - .collect::<Vec<&str>>() - .join(" "); - current.push_str(&range); - } - } - (_, Select::Key(_)) => (), - } - } - _ => { - let stderr = io::stderr(); - let mut stderr = stderr.lock(); - let _ = writeln!(stderr, "ion: invalid array method: {}", self.method); - } + let res = match self.method { + "split" => self.split(expand_func).map(|r| r.join(" ")), + _ => Err("invalid array method"), + }; + match res { + Ok(output) => current.push_str(&output), + Err(msg) => eprintln!("ion: {}: {}", self.method, msg) } } pub(crate) fn handle_as_array<E: Expander>(&self, expand_func: &E) -> Array { - macro_rules! resolve_var { - () => { - if let Some(variable) = expand_func.variable(self.variable, false) { - variable - } else if is_expression(self.variable) { - expand_string(self.variable, expand_func, false).join(" ") + let res = match self.method { + "split" => self.split(expand_func), + "split_at" => self.split_at(expand_func), + "graphemes" => self.graphemes(expand_func), + "bytes" => self.bytes(expand_func), + "chars" => self.chars(expand_func), + _ => Err("invalid array method"), + }; + + res.unwrap_or_else(|m| { + eprintln!("ion: {}: {}", self.method, m); + array![] + }) + } + + #[inline] + fn resolve_var<E: Expander>(&self, expand_func: &E) -> String { + if let Some(variable) = expand_func.variable(self.variable, false) { + variable + } else if is_expression(self.variable) { + expand_string(self.variable, expand_func, false).join(" ") + } else { + "".into() + } + } + + fn split<E: Expander>(&self, expand_func: &E) -> Result<Array, &'static str> { + let variable = self.resolve_var(expand_func); + let res = match (&self.pattern, self.selection.clone()) { + (_, Select::None) => Some("".into()).into_iter().collect(), + (&Pattern::StringPattern(pattern), Select::All) => variable + .split(&unescape(expand_string(pattern, expand_func, false).join(" "))) + .map(From::from) + .collect(), + (&Pattern::Whitespace, Select::All) => variable + .split(char::is_whitespace) + .filter(|x| !x.is_empty()) + .map(From::from) + .collect(), + (&Pattern::StringPattern(pattern), Select::Index(Index::Forward(id))) => { + variable + .split(&unescape(expand_string(pattern, expand_func, false).join(" "))) + .nth(id) + .map(From::from) + .into_iter() + .collect() + } + (&Pattern::Whitespace, Select::Index(Index::Forward(id))) => variable + .split(char::is_whitespace) + .filter(|x| !x.is_empty()) + .nth(id) + .map(From::from) + .into_iter() + .collect(), + (&Pattern::StringPattern(pattern), Select::Index(Index::Backward(id))) => { + variable + .rsplit(&unescape(expand_string(pattern, expand_func, false).join(" "))) + .nth(id) + .map(From::from) + .into_iter() + .collect() + } + (&Pattern::Whitespace, Select::Index(Index::Backward(id))) => variable + .rsplit(char::is_whitespace) + .filter(|x| !x.is_empty()) + .nth(id) + .map(From::from) + .into_iter() + .collect(), + (&Pattern::StringPattern(pattern), Select::Range(range)) => { + let expansion = + unescape(expand_string(pattern, expand_func, false).join(" ")); + let iter = variable.split(&expansion); + if let Some((start, length)) = range.bounds(iter.clone().count()) { + iter.skip(start).take(length).map(From::from).collect() } else { - "".into() + Array::new() } } - } - - match self.method { - "split" => { - let variable = resolve_var!(); - return match (&self.pattern, self.selection.clone()) { - (_, Select::None) => Some("".into()).into_iter().collect(), - (&Pattern::StringPattern(pattern), Select::All) => variable - .split(&unescape(expand_string(pattern, expand_func, false).join(" "))) - .map(From::from) - .collect(), - (&Pattern::Whitespace, Select::All) => variable - .split(char::is_whitespace) - .filter(|x| !x.is_empty()) - .map(From::from) - .collect(), - (&Pattern::StringPattern(pattern), Select::Index(Index::Forward(id))) => { - variable - .split(&unescape(expand_string(pattern, expand_func, false).join(" "))) - .nth(id) - .map(From::from) - .into_iter() - .collect() - } - (&Pattern::Whitespace, Select::Index(Index::Forward(id))) => variable + (&Pattern::Whitespace, Select::Range(range)) => { + let len = + variable.split(char::is_whitespace).filter(|x| !x.is_empty()).count(); + if let Some((start, length)) = range.bounds(len) { + variable .split(char::is_whitespace) .filter(|x| !x.is_empty()) - .nth(id) - .map(From::from) - .into_iter() - .collect(), - (&Pattern::StringPattern(pattern), Select::Index(Index::Backward(id))) => { - variable - .rsplit(&unescape(expand_string(pattern, expand_func, false).join(" "))) - .nth(id) - .map(From::from) - .into_iter() - .collect() - } - (&Pattern::Whitespace, Select::Index(Index::Backward(id))) => variable - .rsplit(char::is_whitespace) - .filter(|x| !x.is_empty()) - .nth(id) + .skip(start) + .take(length) .map(From::from) - .into_iter() - .collect(), - (&Pattern::StringPattern(pattern), Select::Range(range)) => { - let expansion = - unescape(expand_string(pattern, expand_func, false).join(" ")); - let iter = variable.split(&expansion); - if let Some((start, length)) = range.bounds(iter.clone().count()) { - iter.skip(start).take(length).map(From::from).collect() - } else { - Array::new() - } - } - (&Pattern::Whitespace, Select::Range(range)) => { - let len = - variable.split(char::is_whitespace).filter(|x| !x.is_empty()).count(); - if let Some((start, length)) = range.bounds(len) { - variable - .split(char::is_whitespace) - .filter(|x| !x.is_empty()) - .skip(start) - .take(length) - .map(From::from) - .collect() - } else { - Array::new() - } - } - (_, Select::Key(_)) => Some("".into()).into_iter().collect(), - }; - } - "split_at" => { - let variable = resolve_var!(); - match self.pattern { - Pattern::StringPattern(string) => if let Ok(value) = - expand_string(string, expand_func, false).join(" ").parse::<usize>() - { - if value < variable.len() { - let (l, r) = variable.split_at(value); - return array![SmallString::from(l), SmallString::from(r)]; - } - eprintln!("ion: split_at: value is out of bounds"); - } else { - eprintln!("ion: split_at: requires a valid number as an argument"); - }, - Pattern::Whitespace => { - eprintln!("ion: split_at: requires an argument"); - } + .collect() + } else { + Array::new() } } - "graphemes" => { - let variable = resolve_var!(); - let graphemes = UnicodeSegmentation::graphemes(variable.as_str(), true); - let len = graphemes.clone().count(); - return graphemes.map(From::from).select(self.selection.clone(), len); - } - "bytes" => { - let variable = resolve_var!(); - let len = variable.as_bytes().len(); - return variable.bytes().map(|b| b.to_string()).select(self.selection.clone(), len); - } - "chars" => { - let variable = resolve_var!(); - let len = variable.chars().count(); - return variable.chars().map(|c| c.to_string()).select(self.selection.clone(), len); + (_, Select::Key(_)) => Some("".into()).into_iter().collect(), + }; + Ok(res) + } + + fn split_at<E: Expander>(&self, expand_func: &E) -> Result<Array, &'static str> { + let variable = self.resolve_var(expand_func); + match self.pattern { + Pattern::StringPattern(string) => if let Ok(value) = + expand_string(string, expand_func, false).join(" ").parse::<usize>() + { + if value < variable.len() { + let (l, r) = variable.split_at(value); + Ok(array![SmallString::from(l), SmallString::from(r)]) + } else { + Err("value is out of bounds") + } + } else { + Err("requires a valid number as an argument") + }, + Pattern::Whitespace => { + Err("requires an argument") } - _ => { - let stderr = io::stderr(); - let mut stderr = stderr.lock(); - let _ = writeln!(stderr, "ion: invalid array method: {}", self.method); + } + } + + fn graphemes<E: Expander>(&self, expand_func: &E) -> Result<Array, &'static str> { + let variable = self.resolve_var(expand_func); + let graphemes: Vec<String> = UnicodeSegmentation::graphemes(variable.as_str(), true) + .map(From::from) + .collect(); + let len = graphemes.len(); + Ok(graphemes.into_iter().select(self.selection.clone(), len)) + } + + fn bytes<E: Expander>(&self, expand_func: &E) -> Result<Array, &'static str> { + let variable = self.resolve_var(expand_func); + let len = variable.as_bytes().len(); + Ok(variable.bytes().map(|b| b.to_string()).select(self.selection.clone(), len)) + } + + fn chars<E: Expander>(&self, expand_func: &E) -> Result<Array, &'static str> { + let variable = self.resolve_var(expand_func); + let len = variable.chars().count(); + Ok(variable.chars().map(|c| c.to_string()).select(self.selection.clone(), len)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use super::super::Key; + use super::super::super::Range; + use types::Value; + + struct VariableExpander; + + impl Expander for VariableExpander { + fn variable(&self, variable: &str, _: bool) -> Option<Value> { + match variable { + "FOO" => Some("FOOBAR".to_owned()), + "SPACEDFOO" => Some("FOO BAR".to_owned()), + _ => None, } } + } + + #[test] + fn test_split_string_all() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$FOO", + pattern: Pattern::StringPattern("OB"), + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FO AR"); + } + + #[test] + fn test_split_whitespace_all() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::All, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FOO BAR"); + } + + #[test] + fn test_split_string_index_forward() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$FOO", + pattern: Pattern::StringPattern("OB"), + selection: Select::Index(Index::Forward(1)), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "AR"); + } + + #[test] + fn test_split_whitespace_index_forward() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::Index(Index::Forward(1)), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "BAR"); + } + + #[test] + fn test_split_string_index_backward() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$FOO", + pattern: Pattern::StringPattern("OB"), + selection: Select::Index(Index::Backward(1)), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FO"); + } + + #[test] + fn test_split_whitespace_index_backward() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::Index(Index::Backward(1)), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FOO"); + } + + #[test] + fn test_split_string_range() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$FOO", + pattern: Pattern::StringPattern("OB"), + selection: Select::Range(Range::from(Index::Forward(0))), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FO AR"); + } + + #[test] + fn test_split_whitespace_range() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::Range(Range::from(Index::Forward(0))), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, "FOO BAR"); + } + + #[test] + fn test_split_none() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::None, + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, ""); + } + + #[test] + fn test_split_key() { + let mut output = String::new(); + let method = ArrayMethod { + method: "split", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::Key(Key::new("1")), + }; + method.handle(&mut output, &VariableExpander); + assert_eq!(output, ""); + } + + #[test] + fn test_split_at_failing_whitespace() { + let method = ArrayMethod { + method: "split_at", + variable: "$SPACEDFOO", + pattern: Pattern::Whitespace, + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array![]); + } + + #[test] + fn test_split_at_failing_no_number() { + let method = ArrayMethod { + method: "split_at", + variable: "$SPACEDFOO", + pattern: Pattern::StringPattern("a"), + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array![]); + } + + #[test] + fn test_split_at_failing_out_of_bound() { + let method = ArrayMethod { + method: "split_at", + variable: "$SPACEDFOO", + pattern: Pattern::StringPattern("100"), + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array![]); + } + + #[test] + fn test_split_at_succeeding() { + let method = ArrayMethod { + method: "split_at", + variable: "$FOO", + pattern: Pattern::StringPattern("3"), + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array!["FOO", "BAR"]); + } + + #[test] + fn test_graphemes() { + let method = ArrayMethod { + method: "graphemes", + variable: "$FOO", + pattern: Pattern::StringPattern("3"), + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array!["F", "O", "O", "B", "A", "R"]); + } + + #[test] + fn test_bytes() { + let method = ArrayMethod { + method: "bytes", + variable: "$FOO", + pattern: Pattern::StringPattern("3"), + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array!["70", "79", "79", "66", "65", "82"]); + } - array![] + #[test] + fn test_chars() { + let method = ArrayMethod { + method: "chars", + variable: "$FOO", + pattern: Pattern::StringPattern("3"), + selection: Select::All, + }; + assert_eq!(method.handle_as_array(&VariableExpander), array!["F", "O", "O", "B", "A", "R"]); } }