diff --git a/src/uu/test/src/parser.rs b/src/uu/test/src/parser.rs index 2c9c9db30..aa44bc5f2 100644 --- a/src/uu/test/src/parser.rs +++ b/src/uu/test/src/parser.rs @@ -33,7 +33,7 @@ impl Symbol { "(" => Symbol::LParen, "!" => Symbol::Bang, "-a" | "-o" => Symbol::BoolOp(s), - "=" | "!=" => Symbol::StringOp(s), + "=" | "==" | "!=" => Symbol::StringOp(s), "-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s), "-ef" | "-nt" | "-ot" => Symbol::FileOp(s), "-n" | "-z" => Symbol::StrlenOp(s), @@ -83,7 +83,7 @@ impl Symbol { /// TERM → str OP str /// TERM → str | 𝜖 /// OP → STRINGOP | INTOP | FILEOP -/// STRINGOP → = | != +/// STRINGOP → = | == | != /// INTOP → -eq | -ge | -gt | -le | -lt | -ne /// FILEOP → -ef | -nt | -ot /// STRLEN → -n | -z @@ -163,7 +163,7 @@ impl Parser { match self.peek() { // lparen is a literal when followed by nothing or comparison Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => { - self.literal(Symbol::Literal(OsString::from("("))); + self.literal(Symbol::LParen.into_literal()); } // empty parenthetical Symbol::Literal(s) if s == ")" => {} @@ -183,27 +183,67 @@ impl Parser { /// /// * `! =`: negate the result of the implicit string length test of `=` /// * `! = foo`: compare the literal strings `!` and `foo` - /// * `! `: negate the result of the expression + /// * `! = = str`: negate comparison of literal `=` and `str` + /// * `!`: bang followed by nothing is literal + /// * `! EXPR`: negate the result of the expression + /// + /// Combined Boolean & negation: + /// + /// * `! ( EXPR ) [BOOLOP EXPR]`: negate the parenthesized expression only + /// * `! UOP str BOOLOP EXPR`: negate the unary subexpression + /// * `! str BOOLOP str`: negate the entire Boolean expression + /// * `! str BOOLOP EXPR BOOLOP EXPR`: negate the value of the first `str` term /// fn bang(&mut self) { - if let Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) = self.peek() { - // we need to peek ahead one more token to disambiguate the first - // two cases listed above: case 1 — `! ` — and - // case 2: ` OP str`. - let peek2 = self.tokens.clone().nth(1); + match self.peek() { + Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) | Symbol::BoolOp(_) => { + // we need to peek ahead one more token to disambiguate the first + // three cases listed above + let peek2 = Symbol::new(self.tokens.clone().nth(1)); - if peek2.is_none() { - // op is literal - let op = self.next_token().into_literal(); - self.stack.push(op); - self.stack.push(Symbol::Bang); - } else { - // bang is literal; parsing continues with op - self.literal(Symbol::Literal(OsString::from("!"))); + match peek2 { + // case 1: `! ` + // case 3: `! = OP str` + Symbol::StringOp(_) | Symbol::None => { + // op is literal + let op = self.next_token().into_literal(); + self.literal(op); + self.stack.push(Symbol::Bang); + } + // case 2: ` OP str [BOOLOP EXPR]`. + _ => { + // bang is literal; parsing continues with op + self.literal(Symbol::Bang.into_literal()); + self.maybe_boolop(); + } + } + } + + // bang followed by nothing is literal + Symbol::None => self.stack.push(Symbol::Bang.into_literal()), + + _ => { + // peek ahead up to 4 tokens to determine if we need to negate + // the entire expression or just the first term + let peek4: Vec = self + .tokens + .clone() + .take(4) + .map(|token| Symbol::new(Some(token))) + .collect(); + + match peek4.as_slice() { + // we peeked ahead 4 but there were only 3 tokens left + [Symbol::Literal(_), Symbol::BoolOp(_), Symbol::Literal(_)] => { + self.expr(); + self.stack.push(Symbol::Bang); + } + _ => { + self.term(); + self.stack.push(Symbol::Bang); + } + } } - } else { - self.expr(); - self.stack.push(Symbol::Bang); } } @@ -211,13 +251,14 @@ impl Parser { /// as appropriate. fn maybe_boolop(&mut self) { if self.peek_is_boolop() { - let token = self.tokens.next().unwrap(); // safe because we peeked + let symbol = self.next_token(); // BoolOp by itself interpreted as Literal if let Symbol::None = self.peek() { - self.literal(Symbol::Literal(token)) + self.literal(symbol.into_literal()); } else { - self.boolop(Symbol::BoolOp(token)) + self.boolop(symbol); + self.maybe_boolop(); } } } @@ -231,7 +272,6 @@ impl Parser { if op == Symbol::BoolOp(OsString::from("-a")) { self.term(); self.stack.push(op); - self.maybe_boolop(); } else { self.expr(); self.stack.push(op); diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 3e97af0a6..86950ecc2 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -57,7 +57,7 @@ fn eval(stack: &mut Vec) -> Result { Some(Symbol::StringOp(op)) => { let b = stack.pop(); let a = stack.pop(); - Ok(if op == "=" { a == b } else { a != b }) + Ok(if op == "!=" { a != b } else { a == b }) } Some(Symbol::IntOp(op)) => { let b = pop_literal!(); diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 000013d9c..0dfc0c620 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -122,6 +122,13 @@ fn test_zero_len_not_equals_zero_len_is_false() { new_ucmd!().args(&["", "!=", ""]).run().status_code(1); } +#[test] +fn test_double_equal_is_string_comparison_op() { + // undocumented but part of the GNU test suite + new_ucmd!().args(&["t", "==", "t"]).succeeds(); + new_ucmd!().args(&["t", "==", "f"]).run().status_code(1); +} + #[test] fn test_string_comparison() { let scenario = TestScenario::new(util_name!()); @@ -131,11 +138,22 @@ fn test_string_comparison() { ["(", "=", "("], ["(", "!=", ")"], ["!", "=", "!"], + ["=", "=", "="], ]; for test in &tests { scenario.ucmd().args(&test[..]).succeeds(); } + + // run the inverse of all these tests + for test in &tests { + scenario + .ucmd() + .arg("!") + .args(&test[..]) + .run() + .status_code(1); + } } #[test] @@ -485,6 +503,81 @@ fn test_op_prec_and_or_2_overridden_by_parentheses() { .status_code(1); } +#[test] +fn test_negated_boolean_precedence() { + let scenario = TestScenario::new(util_name!()); + + let tests = [ + vec!["!", "(", "foo", ")", "-o", "bar"], + vec!["!", "", "-o", "", "-a", ""], + vec!["!", "(", "", "-a", "", ")", "-o", ""], + ]; + + for test in &tests { + scenario.ucmd().args(&test[..]).succeeds(); + } + + let negative_tests = [ + vec!["!", "-n", "", "-a", ""], + vec!["", "-a", "", "-o", ""], + vec!["!", "", "-a", "", "-o", ""], + vec!["!", "(", "", "-a", "", ")", "-a", ""], + ]; + + for test in &negative_tests { + scenario.ucmd().args(&test[..]).run().status_code(1); + } +} + +#[test] +fn test_bang_boolop_precedence() { + // For a Boolean combination of two literals, bang inverts the entire expression + new_ucmd!().args(&["!", "", "-a", ""]).succeeds(); + new_ucmd!().args(&["!", "", "-o", ""]).succeeds(); + + new_ucmd!() + .args(&["!", "a value", "-o", "another value"]) + .run() + .status_code(1); + + // Introducing a UOP — even one that is equivalent to a bare string — causes + // bang to invert only the first term + new_ucmd!() + .args(&["!", "-n", "", "-a", ""]) + .run() + .status_code(1); + new_ucmd!() + .args(&["!", "", "-a", "-n", ""]) + .run() + .status_code(1); + + // for compound Boolean expressions, bang inverts the _next_ expression + // only, not the entire compound expression + new_ucmd!() + .args(&["!", "", "-a", "", "-a", ""]) + .run() + .status_code(1); + + // parentheses can override this + new_ucmd!() + .args(&["!", "(", "", "-a", "", "-a", "", ")"]) + .succeeds(); +} + +#[test] +fn test_inverted_parenthetical_boolop_precedence() { + // For a Boolean combination of two literals, bang inverts the entire expression + new_ucmd!() + .args(&["!", "a value", "-o", "another value"]) + .run() + .status_code(1); + + // only the parenthetical is inverted, not the entire expression + new_ucmd!() + .args(&["!", "(", "a value", ")", "-o", "another value"]) + .succeeds(); +} + #[test] #[ignore = "fixme: error reporting"] fn test_dangling_parenthesis() {