1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

test: improve handling of inverted Boolean expressions

- add `==` as undocumented alias of `=`

- handle negated comparison of `=` as literal

- negation generally applies to only the first expression of a Boolean chain,
  except when combining evaluation of two literal strings
This commit is contained in:
Daniel Rocco 2021-05-06 08:28:54 -04:00
parent ed42652803
commit 2ec4bee350
3 changed files with 158 additions and 25 deletions

View file

@ -33,7 +33,7 @@ impl Symbol {
"(" => Symbol::LParen, "(" => Symbol::LParen,
"!" => Symbol::Bang, "!" => Symbol::Bang,
"-a" | "-o" => Symbol::BoolOp(s), "-a" | "-o" => Symbol::BoolOp(s),
"=" | "!=" => Symbol::StringOp(s), "=" | "==" | "!=" => Symbol::StringOp(s),
"-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s), "-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s),
"-ef" | "-nt" | "-ot" => Symbol::FileOp(s), "-ef" | "-nt" | "-ot" => Symbol::FileOp(s),
"-n" | "-z" => Symbol::StrlenOp(s), "-n" | "-z" => Symbol::StrlenOp(s),
@ -83,7 +83,7 @@ impl Symbol {
/// TERM → str OP str /// TERM → str OP str
/// TERM → str | 𝜖 /// TERM → str | 𝜖
/// OP → STRINGOP | INTOP | FILEOP /// OP → STRINGOP | INTOP | FILEOP
/// STRINGOP → = | != /// STRINGOP → = | == | !=
/// INTOP → -eq | -ge | -gt | -le | -lt | -ne /// INTOP → -eq | -ge | -gt | -le | -lt | -ne
/// FILEOP → -ef | -nt | -ot /// FILEOP → -ef | -nt | -ot
/// STRLEN → -n | -z /// STRLEN → -n | -z
@ -163,7 +163,7 @@ impl Parser {
match self.peek() { match self.peek() {
// lparen is a literal when followed by nothing or comparison // lparen is a literal when followed by nothing or comparison
Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => { Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => {
self.literal(Symbol::Literal(OsString::from("("))); self.literal(Symbol::LParen.into_literal());
} }
// empty parenthetical // empty parenthetical
Symbol::Literal(s) if s == ")" => {} Symbol::Literal(s) if s == ")" => {}
@ -183,27 +183,67 @@ impl Parser {
/// ///
/// * `! =`: negate the result of the implicit string length test of `=` /// * `! =`: negate the result of the implicit string length test of `=`
/// * `! = foo`: compare the literal strings `!` and `foo` /// * `! = foo`: compare the literal strings `!` and `foo`
/// * `! <expr>`: 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) { fn bang(&mut self) {
if let Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) = self.peek() { match self.peek() {
// we need to peek ahead one more token to disambiguate the first Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) | Symbol::BoolOp(_) => {
// two cases listed above: case 1 — `! <OP as literal>` — and // we need to peek ahead one more token to disambiguate the first
// case 2: `<! as literal> OP str`. // three cases listed above
let peek2 = self.tokens.clone().nth(1); let peek2 = Symbol::new(self.tokens.clone().nth(1));
if peek2.is_none() { match peek2 {
// op is literal // case 1: `! <OP as literal>`
let op = self.next_token().into_literal(); // case 3: `! = OP str`
self.stack.push(op); Symbol::StringOp(_) | Symbol::None => {
self.stack.push(Symbol::Bang); // op is literal
} else { let op = self.next_token().into_literal();
// bang is literal; parsing continues with op self.literal(op);
self.literal(Symbol::Literal(OsString::from("!"))); self.stack.push(Symbol::Bang);
}
// case 2: `<! as literal> 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<Symbol> = 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. /// as appropriate.
fn maybe_boolop(&mut self) { fn maybe_boolop(&mut self) {
if self.peek_is_boolop() { 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 // BoolOp by itself interpreted as Literal
if let Symbol::None = self.peek() { if let Symbol::None = self.peek() {
self.literal(Symbol::Literal(token)) self.literal(symbol.into_literal());
} else { } 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")) { if op == Symbol::BoolOp(OsString::from("-a")) {
self.term(); self.term();
self.stack.push(op); self.stack.push(op);
self.maybe_boolop();
} else { } else {
self.expr(); self.expr();
self.stack.push(op); self.stack.push(op);

View file

@ -57,7 +57,7 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
Some(Symbol::StringOp(op)) => { Some(Symbol::StringOp(op)) => {
let b = stack.pop(); let b = stack.pop();
let a = 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)) => { Some(Symbol::IntOp(op)) => {
let b = pop_literal!(); let b = pop_literal!();

View file

@ -122,6 +122,13 @@ fn test_zero_len_not_equals_zero_len_is_false() {
new_ucmd!().args(&["", "!=", ""]).run().status_code(1); 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] #[test]
fn test_string_comparison() { fn test_string_comparison() {
let scenario = TestScenario::new(util_name!()); let scenario = TestScenario::new(util_name!());
@ -131,11 +138,22 @@ fn test_string_comparison() {
["(", "=", "("], ["(", "=", "("],
["(", "!=", ")"], ["(", "!=", ")"],
["!", "=", "!"], ["!", "=", "!"],
["=", "=", "="],
]; ];
for test in &tests { for test in &tests {
scenario.ucmd().args(&test[..]).succeeds(); 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] #[test]
@ -485,6 +503,81 @@ fn test_op_prec_and_or_2_overridden_by_parentheses() {
.status_code(1); .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] #[test]
#[ignore = "fixme: error reporting"] #[ignore = "fixme: error reporting"]
fn test_dangling_parenthesis() { fn test_dangling_parenthesis() {