mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37: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::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,41 +183,82 @@ 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() {
|
||||||
|
Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) | Symbol::BoolOp(_) => {
|
||||||
// we need to peek ahead one more token to disambiguate the first
|
// we need to peek ahead one more token to disambiguate the first
|
||||||
// two cases listed above: case 1 — `! <OP as literal>` — and
|
// three cases listed above
|
||||||
// case 2: `<! as literal> OP str`.
|
let peek2 = Symbol::new(self.tokens.clone().nth(1));
|
||||||
let peek2 = self.tokens.clone().nth(1);
|
|
||||||
|
|
||||||
if peek2.is_none() {
|
match peek2 {
|
||||||
|
// case 1: `! <OP as literal>`
|
||||||
|
// case 3: `! = OP str`
|
||||||
|
Symbol::StringOp(_) | Symbol::None => {
|
||||||
// op is literal
|
// op is literal
|
||||||
let op = self.next_token().into_literal();
|
let op = self.next_token().into_literal();
|
||||||
self.stack.push(op);
|
self.literal(op);
|
||||||
self.stack.push(Symbol::Bang);
|
self.stack.push(Symbol::Bang);
|
||||||
} else {
|
|
||||||
// bang is literal; parsing continues with op
|
|
||||||
self.literal(Symbol::Literal(OsString::from("!")));
|
|
||||||
}
|
}
|
||||||
} else {
|
// 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.expr();
|
||||||
self.stack.push(Symbol::Bang);
|
self.stack.push(Symbol::Bang);
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
|
self.term();
|
||||||
|
self.stack.push(Symbol::Bang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peek at the next token and parse it as a BOOLOP or string literal,
|
/// Peek at the next token and parse it as a BOOLOP or string literal,
|
||||||
/// 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);
|
||||||
|
|
|
@ -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!();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue