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:
parent
ed42652803
commit
2ec4bee350
3 changed files with 158 additions and 25 deletions
|
@ -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`
|
||||
/// * `! <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) {
|
||||
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 — `! <OP as literal>` — and
|
||||
// case 2: `<! as literal> 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: `! <OP as literal>`
|
||||
// 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: `<! 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.
|
||||
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);
|
||||
|
|
|
@ -57,7 +57,7 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
|
|||
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!();
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue