diff --git a/src/uu/test/src/parser.rs b/src/uu/test/src/parser.rs index dab0f8af6f9..85b9c5c5f0c 100644 --- a/src/uu/test/src/parser.rs +++ b/src/uu/test/src/parser.rs @@ -195,10 +195,11 @@ impl Parser { /// /// EXPR → TERM | EXPR BOOLOP EXPR fn expr(&mut self) -> ParseResult<()> { - if !self.peek_is_boolop() { + let has_term = !self.peek_is_boolop(); + if has_term { self.term()?; } - self.maybe_boolop()?; + self.maybe_boolop(has_term)?; Ok(()) } @@ -344,7 +345,7 @@ impl Parser { _ => { // bang is literal; parsing continues with op self.literal(Symbol::Bang.into_literal())?; - self.maybe_boolop()?; + self.maybe_boolop(true)?; } } } @@ -379,16 +380,28 @@ impl Parser { /// Peek at the next token and parse it as a BOOLOP or string literal, /// as appropriate. - fn maybe_boolop(&mut self) -> ParseResult<()> { + /// + /// `has_left_operand` tells whether an expression was already parsed for + /// the BOOLOP to apply to, which decides what a BOOLOP at the end of the + /// stream means. + fn maybe_boolop(&mut self, has_left_operand: bool) -> ParseResult<()> { if self.peek_is_boolop() { let symbol = self.next_token(); - // BoolOp by itself interpreted as Literal if let Symbol::None = self.peek() { + if has_left_operand { + // The BOOLOP joins the expression so far to nothing. + return Err(ParseError::at_token( + ParseErrorKind::MissingArgument(format!("{symbol}")), + self.last_pos(), + )); + } + // With no operand on either side it is an ordinary string: + // `test -a` is the length test of the string "-a". self.literal(symbol.into_literal())?; } else { self.boolop(symbol)?; - self.maybe_boolop()?; + self.maybe_boolop(true)?; } } Ok(()) diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index c044d46076d..654ae086e14 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -1140,11 +1140,40 @@ fn test_filename_or_with_equal() { } #[test] -#[ignore = "GNU considers this an error"] fn test_string_length_and_nothing() { new_ucmd!().args(&["-n", "a", "-a"]).fails_with_code(2); } +#[test] +fn test_boolop_without_right_operand() { + for op in ["-a", "-o"] { + new_ucmd!() + .args(&["x", op]) + .fails_with_code(2) + .stderr_is(format!("test: missing argument after '{op}'\n")); + + // An empty left operand is still an operand. + new_ucmd!().args(&["", op]).fails_with_code(2); + + // Whatever precedes it, the trailing BOOLOP has nothing to join to. + new_ucmd!().args(&["x", "-a", "y", op]).fails_with_code(2); + new_ucmd!().args(&["(", "x", ")", op]).fails_with_code(2); + new_ucmd!().args(&["!", "x", op]).fails_with_code(2); + } +} + +#[test] +fn test_lone_boolop_is_a_string() { + // With no operand on either side, -a and -o are ordinary strings. + for op in ["-a", "-o"] { + new_ucmd!().arg(op).succeeds(); + new_ucmd!().args(&["!", op]).fails_with_code(1); + } + + // A unary operator still takes the BOOLOP as its operand. + new_ucmd!().args(&["-n", "-a"]).succeeds(); +} + #[test] fn test_bracket_syntax_success() { let scenario = TestScenario::new("[");