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

test: implement parenthesized expressions, additional tests

- Replace the parser with a recursive descent implementation that handles
  parentheses and produces a stack of operations in postfix order.

  Parsing now operates directly on OsStrings passed by the uucore framework.

- Replace the dispatch mechanism with a stack machine operating on the
  symbol stack produced by the parser.

- Add tests for parenthesized expressions.

- Begin testing character encoding handling.
This commit is contained in:
Daniel Rocco 2021-04-26 21:27:29 -04:00
parent c3912d53ac
commit 3c126bad72
3 changed files with 504 additions and 308 deletions

292
src/uu/test/src/parser.rs Normal file
View file

@ -0,0 +1,292 @@
// This file is part of the uutils coreutils package.
//
// (c) Daniel Rocco <drocco@gmail.com>
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ffi::OsString;
use std::iter::Peekable;
/// Represents a parsed token from a test expression
#[derive(Debug, PartialEq)]
pub enum Symbol {
LParen,
Bang,
BoolOp(OsString),
Literal(OsString),
StringOp(OsString),
IntOp(OsString),
FileOp(OsString),
StrlenOp(OsString),
FiletestOp(OsString),
None,
}
impl Symbol {
/// Create a new Symbol from an OsString.
///
/// Returns Symbol::None in place of None
fn new(token: Option<OsString>) -> Symbol {
match token {
Some(s) => match s.to_string_lossy().as_ref() {
"(" => Symbol::LParen,
"!" => Symbol::Bang,
"-a" | "-o" => Symbol::BoolOp(s),
"=" | "!=" => Symbol::StringOp(s),
"-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s),
"-ef" | "-nt" | "-ot" => Symbol::FileOp(s),
"-n" | "-z" => Symbol::StrlenOp(s),
"-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-G" | "-h" | "-k" | "-L" | "-O"
| "-p" | "-r" | "-s" | "-S" | "-t" | "-u" | "-w" | "-x" => Symbol::FiletestOp(s),
_ => Symbol::Literal(s),
},
None => Symbol::None,
}
}
/// Convert this Symbol into a Symbol::Literal, useful for cases where
/// test treats an operator as a string operand (test has no reserved
/// words).
///
/// # Panics
///
/// Panics if `self` is Symbol::None
fn into_literal(self) -> Symbol {
Symbol::Literal(match self {
Symbol::LParen => OsString::from("("),
Symbol::Bang => OsString::from("!"),
Symbol::BoolOp(s)
| Symbol::Literal(s)
| Symbol::StringOp(s)
| Symbol::IntOp(s)
| Symbol::FileOp(s)
| Symbol::StrlenOp(s)
| Symbol::FiletestOp(s) => s,
Symbol::None => panic!(),
})
}
}
/// Recursive descent parser for test, which converts a list of OsStrings
/// (typically command line arguments) into a stack of Symbols in postfix
/// order.
///
/// Grammar:
///
/// EXPR → TERM | EXPR BOOLOP EXPR
/// TERM → ( EXPR )
/// TERM → ( )
/// TERM → ! EXPR
/// TERM → UOP str
/// UOP → STRLEN | FILETEST
/// TERM → str OP str
/// TERM → str | 𝜖
/// OP → STRINGOP | INTOP | FILEOP
/// STRINGOP → = | !=
/// INTOP → -eq | -ge | -gt | -le | -lt | -ne
/// FILEOP → -ef | -nt | -ot
/// STRLEN → -n | -z
/// FILETEST → -b | -c | -d | -e | -f | -g | -G | -h | -k | -L | -O | -p |
/// -r | -s | -S | -t | -u | -w | -x
/// BOOLOP → -a | -o
///
#[derive(Debug)]
struct Parser {
tokens: Peekable<std::vec::IntoIter<OsString>>,
pub stack: Vec<Symbol>,
}
impl Parser {
/// Construct a new Parser from a `Vec<OsString>` of tokens.
fn new(tokens: Vec<OsString>) -> Parser {
Parser {
tokens: tokens.into_iter().peekable(),
stack: vec![],
}
}
/// Fetch the next token from the input stream as a Symbol.
fn next_token(&mut self) -> Symbol {
Symbol::new(self.tokens.next())
}
/// Peek at the next token from the input stream, returning it as a Symbol.
/// The stream is unchanged and will return the same Symbol on subsequent
/// calls to `next()` or `peek()`.
fn peek(&mut self) -> Symbol {
Symbol::new(self.tokens.peek().map(|s| s.to_os_string()))
}
/// Test if the next token in the stream is a BOOLOP (-a or -o), without
/// removing the token from the stream.
fn peek_is_boolop(&mut self) -> bool {
if let Symbol::BoolOp(_) = self.peek() {
true
} else {
false
}
}
/// Parse an expression.
///
/// EXPR → TERM | EXPR BOOLOP EXPR
fn expr(&mut self) {
if !self.peek_is_boolop() {
self.term();
}
self.maybe_boolop();
}
/// Parse a term token and possible subsequent symbols: "(", "!", UOP,
/// literal, or None.
fn term(&mut self) {
let symbol = self.next_token();
match symbol {
Symbol::LParen => self.lparen(),
Symbol::Bang => self.bang(),
Symbol::StrlenOp(_) => self.uop(symbol),
Symbol::FiletestOp(_) => self.uop(symbol),
Symbol::None => self.stack.push(symbol),
literal => self.literal(literal),
}
}
/// Parse a (possibly) parenthesized expression.
///
/// test has no reserved keywords, so "(" will be interpreted as a literal
/// if it is followed by nothing or a comparison operator OP.
fn lparen(&mut self) {
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("(")));
}
// empty parenthetical
Symbol::Literal(s) if s == ")" => {}
_ => {
self.expr();
match self.next_token() {
Symbol::Literal(s) if s == ")" => (),
_ => panic!("expected )"),
}
}
}
}
/// Parse a (possibly) negated expression.
///
/// Example cases:
///
/// * `! =`: negate the result of the implicit string length test of `=`
/// * `! = foo`: compare the literal strings `!` and `foo`
/// * `! <expr>`: negate the result of the expression
///
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);
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("!")));
}
} else {
self.expr();
self.stack.push(Symbol::Bang);
}
}
/// Peek at the next token and parse it as a BOOLOP or string literal,
/// as appropriate.
fn maybe_boolop(&mut self) {
if self.peek_is_boolop() {
let token = self.tokens.next().unwrap(); // safe because we peeked
// BoolOp by itself interpreted as Literal
if let Symbol::None = self.peek() {
self.literal(Symbol::Literal(token))
} else {
self.boolop(Symbol::BoolOp(token))
}
}
}
/// Parse a Boolean expression.
///
/// Logical and (-a) has higher precedence than or (-o), so in an
/// expression like `foo -o '' -a ''`, the and subexpression is evaluated
/// first.
fn boolop(&mut self, op: Symbol) {
if op == Symbol::BoolOp(OsString::from("-a")) {
self.term();
self.stack.push(op);
self.maybe_boolop();
} else {
self.expr();
self.stack.push(op);
}
}
/// Parse a (possible) unary argument test (string length or file
/// attribute check).
///
/// If a UOP is followed by nothing it is interpreted as a literal string.
fn uop(&mut self, op: Symbol) {
match self.next_token() {
Symbol::None => self.stack.push(op.into_literal()),
symbol => {
self.stack.push(symbol.into_literal());
self.stack.push(op);
}
}
}
/// Parse a string literal, optionally followed by a comparison operator
/// and a second string literal.
fn literal(&mut self, token: Symbol) {
self.stack.push(token.into_literal());
// EXPR → str OP str
match self.peek() {
Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => {
let op = self.next_token();
match self.next_token() {
Symbol::None => panic!("missing argument after {:?}", op),
token => self.stack.push(token.into_literal()),
}
self.stack.push(op);
}
_ => {}
}
}
/// Parser entry point: parse the token stream `self.tokens`, storing the
/// resulting `Symbol` stack in `self.stack`.
fn parse(&mut self) -> Result<(), String> {
self.expr();
match self.tokens.next() {
Some(token) => Err(format!("extra argument {}", token.to_string_lossy())),
None => Ok(()),
}
}
}
/// Parse the token stream `args`, returning a `Symbol` stack representing the
/// operations to perform in postfix order.
pub fn parse(args: Vec<OsString>) -> Result<Vec<Symbol>, String> {
let mut p = Parser::new(args);
p.parse()?;
Ok(p.stack)
}

View file

@ -1,144 +1,154 @@
// * This file is part of the uutils coreutils package.
// *
// * (c) mahkoh (ju.orth [at] gmail [dot] com)
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
// This file is part of the uutils coreutils package.
//
// (c) mahkoh (ju.orth [at] gmail [dot] com)
// (c) Daniel Rocco <drocco@gmail.com>
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) retval paren prec subprec cond
use std::collections::HashMap;
use std::str::from_utf8;
mod parser;
static NAME: &str = "test";
use parser::{parse, Symbol};
use std::ffi::{OsStr, OsString};
pub fn uumain(args: impl uucore::Args) -> i32 {
let args: Vec<_> = args.collect();
// This is completely disregarding valid windows paths that aren't valid unicode
let args = args
.iter()
.map(|a| a.to_str().unwrap().as_bytes())
.collect::<Vec<&[u8]>>();
if args.is_empty() {
return 2;
}
let args = if !args[0].ends_with(NAME.as_bytes()) {
&args[1..]
// TODO: handle being called as `[`
let args: Vec<_> = args.skip(1).collect();
let result = parse(args).and_then(|mut stack| eval(&mut stack));
match result {
Ok(result) => {
if result {
0
} else {
&args[..]
};
let args = match args[0] {
b"[" => match args[args.len() - 1] {
b"]" => &args[1..args.len() - 1],
_ => return 2,
},
_ => &args[1..args.len()],
};
let mut error = false;
let retval = 1 - parse_expr(args, &mut error) as i32;
if error {
1
}
}
Err(e) => {
eprintln!("test: {}", e);
2
}
}
}
/// Evaluate a stack of Symbols, returning the result of the evaluation or
/// an error message if evaluation failed.
fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
macro_rules! pop_literal {
() => {
match stack.pop() {
Some(Symbol::Literal(s)) => s,
_ => panic!(),
}
};
}
let s = stack.pop();
match s {
Some(Symbol::Bang) => {
let result = eval(stack)?;
Ok(!result)
}
Some(Symbol::StringOp(op)) => {
let b = stack.pop();
let a = stack.pop();
Ok(if op == "=" { a == b } else { a != b })
}
Some(Symbol::IntOp(op)) => {
let b = pop_literal!();
let a = pop_literal!();
Ok(integers(&a, &b, &op)?)
}
Some(Symbol::FileOp(_op)) => unimplemented!(),
Some(Symbol::StrlenOp(op)) => {
let s = match stack.pop() {
Some(Symbol::Literal(s)) => s,
Some(Symbol::None) => OsString::from(""),
None => {
return Ok(true);
}
_ => {
return Err(format!("missing argument after {:?}", op));
}
};
Ok(if op == "-z" {
s.is_empty()
} else {
retval
!s.is_empty()
})
}
Some(Symbol::FiletestOp(op)) => {
let op = op.to_string_lossy();
let f = pop_literal!();
Ok(match op.as_ref() {
"-b" => path(&f, PathCondition::BlockSpecial),
"-c" => path(&f, PathCondition::CharacterSpecial),
"-d" => path(&f, PathCondition::Directory),
"-e" => path(&f, PathCondition::Exists),
"-f" => path(&f, PathCondition::Regular),
"-g" => path(&f, PathCondition::GroupIdFlag),
"-h" => path(&f, PathCondition::SymLink),
"-L" => path(&f, PathCondition::SymLink),
"-p" => path(&f, PathCondition::Fifo),
"-r" => path(&f, PathCondition::Readable),
"-S" => path(&f, PathCondition::Socket),
"-s" => path(&f, PathCondition::NonEmpty),
"-t" => isatty(&f)?,
"-u" => path(&f, PathCondition::UserIdFlag),
"-w" => path(&f, PathCondition::Writable),
"-x" => path(&f, PathCondition::Executable),
_ => panic!(),
})
}
Some(Symbol::Literal(s)) => Ok(!s.is_empty()),
Some(Symbol::None) => Ok(false),
Some(Symbol::BoolOp(op)) => {
let b = eval(stack)?;
let a = eval(stack)?;
Ok(if op == "-a" { a && b } else { a || b })
}
None => Ok(false),
_ => Err("expected value".to_string()),
}
}
fn one(args: &[&[u8]]) -> bool {
!args[0].is_empty()
fn integers(a: &OsStr, b: &OsStr, cond: &OsStr) -> Result<bool, String> {
let format_err = |value| format!("invalid integer {}", value);
let a = a.to_string_lossy();
let a: i64 = a.parse().map_err(|_| format_err(a))?;
let b = b.to_string_lossy();
let b: i64 = b.parse().map_err(|_| format_err(b))?;
let cond = cond.to_string_lossy();
Ok(match cond.as_ref() {
"-eq" => a == b,
"-ne" => a != b,
"-gt" => a > b,
"-ge" => a >= b,
"-lt" => a < b,
"-le" => a <= b,
_ => return Err(format!("unknown operator {}", cond)),
})
}
fn two(args: &[&[u8]], error: &mut bool) -> bool {
match args[0] {
b"!" => !one(&args[1..]),
b"-b" => path(args[1], PathCondition::BlockSpecial),
b"-c" => path(args[1], PathCondition::CharacterSpecial),
b"-d" => path(args[1], PathCondition::Directory),
b"-e" => path(args[1], PathCondition::Exists),
b"-f" => path(args[1], PathCondition::Regular),
b"-g" => path(args[1], PathCondition::GroupIdFlag),
b"-h" => path(args[1], PathCondition::SymLink),
b"-L" => path(args[1], PathCondition::SymLink),
b"-n" => one(&args[1..]),
b"-p" => path(args[1], PathCondition::Fifo),
b"-r" => path(args[1], PathCondition::Readable),
b"-S" => path(args[1], PathCondition::Socket),
b"-s" => path(args[1], PathCondition::NonEmpty),
b"-t" => isatty(args[1]),
b"-u" => path(args[1], PathCondition::UserIdFlag),
b"-w" => path(args[1], PathCondition::Writable),
b"-x" => path(args[1], PathCondition::Executable),
b"-z" => !one(&args[1..]),
_ => {
*error = true;
false
}
}
}
fn isatty(fd: &OsStr) -> Result<bool, String> {
let fd = fd.to_string_lossy();
fn three(args: &[&[u8]], error: &mut bool) -> bool {
match args[1] {
b"=" => args[0] == args[2],
b"==" => args[0] == args[2],
b"!=" => args[0] != args[2],
b"-eq" => integers(args[0], args[2], IntegerCondition::Equal),
b"-ne" => integers(args[0], args[2], IntegerCondition::Unequal),
b"-gt" => integers(args[0], args[2], IntegerCondition::Greater),
b"-ge" => integers(args[0], args[2], IntegerCondition::GreaterEqual),
b"-lt" => integers(args[0], args[2], IntegerCondition::Less),
b"-le" => integers(args[0], args[2], IntegerCondition::LessEqual),
_ => match args[0] {
b"!" => !two(&args[1..], error),
_ => {
*error = true;
false
}
},
}
}
fn four(args: &[&[u8]], error: &mut bool) -> bool {
match args[0] {
b"!" => !three(&args[1..], error),
_ => {
*error = true;
false
}
}
}
enum IntegerCondition {
Equal,
Unequal,
Greater,
GreaterEqual,
Less,
LessEqual,
}
fn integers(a: &[u8], b: &[u8], cond: IntegerCondition) -> bool {
let (a, b): (&str, &str) = match (from_utf8(a), from_utf8(b)) {
(Ok(a), Ok(b)) => (a, b),
_ => return false,
};
let (a, b): (i64, i64) = match (a.parse(), b.parse()) {
(Ok(a), Ok(b)) => (a, b),
_ => return false,
};
match cond {
IntegerCondition::Equal => a == b,
IntegerCondition::Unequal => a != b,
IntegerCondition::Greater => a > b,
IntegerCondition::GreaterEqual => a >= b,
IntegerCondition::Less => a < b,
IntegerCondition::LessEqual => a <= b,
}
}
fn isatty(fd: &[u8]) -> bool {
from_utf8(fd)
.ok()
.and_then(|s| s.parse().ok())
.map_or(false, |i| {
fd.parse()
.map_err(|_| format!("invalid integer {}", fd))
.map(|i| {
#[cfg(not(target_os = "redox"))]
unsafe {
libc::isatty(i) == 1
@ -148,173 +158,6 @@ fn isatty(fd: &[u8]) -> bool {
})
}
fn dispatch(args: &mut &[&[u8]], error: &mut bool) -> bool {
let (val, idx) = match args.len() {
0 => {
*error = true;
(false, 0)
}
1 => (one(*args), 1),
2 => dispatch_two(args, error),
3 => dispatch_three(args, error),
_ => dispatch_four(args, error),
};
*args = &(*args)[idx..];
val
}
fn dispatch_two(args: &mut &[&[u8]], error: &mut bool) -> (bool, usize) {
let val = two(*args, error);
if *error {
*error = false;
(one(*args), 1)
} else {
(val, 2)
}
}
fn dispatch_three(args: &mut &[&[u8]], error: &mut bool) -> (bool, usize) {
let val = three(*args, error);
if *error {
*error = false;
dispatch_two(args, error)
} else {
(val, 3)
}
}
fn dispatch_four(args: &mut &[&[u8]], error: &mut bool) -> (bool, usize) {
let val = four(*args, error);
if *error {
*error = false;
dispatch_three(args, error)
} else {
(val, 4)
}
}
#[derive(Clone, Copy)]
enum Precedence {
Unknown = 0,
Paren, // FIXME: this is useless (parentheses have not been implemented)
Or,
And,
BUnOp,
BinOp,
UnOp,
}
fn parse_expr(mut args: &[&[u8]], error: &mut bool) -> bool {
if args.is_empty() {
false
} else {
let hashmap = setup_hashmap();
let lhs = dispatch(&mut args, error);
if !args.is_empty() {
parse_expr_helper(&hashmap, &mut args, lhs, Precedence::Unknown, error)
} else {
lhs
}
}
}
fn parse_expr_helper<'a>(
hashmap: &HashMap<&'a [u8], Precedence>,
args: &mut &[&'a [u8]],
mut lhs: bool,
min_prec: Precedence,
error: &mut bool,
) -> bool {
let mut prec = *hashmap.get(&args[0]).unwrap_or_else(|| {
*error = true;
&min_prec
});
while !*error && !args.is_empty() && prec as usize >= min_prec as usize {
let op = args[0];
*args = &(*args)[1..];
let mut rhs = dispatch(args, error);
while !args.is_empty() {
let subprec = *hashmap.get(&args[0]).unwrap_or_else(|| {
*error = true;
&min_prec
});
if subprec as usize <= prec as usize || *error {
break;
}
rhs = parse_expr_helper(hashmap, args, rhs, subprec, error);
}
lhs = match prec {
Precedence::UnOp | Precedence::BUnOp => {
*error = true;
false
}
Precedence::And => lhs && rhs,
Precedence::Or => lhs || rhs,
Precedence::BinOp => three(
&[
if lhs { b" " } else { b"" },
op,
if rhs { b" " } else { b"" },
],
error,
),
Precedence::Paren => unimplemented!(), // TODO: implement parentheses
_ => unreachable!(),
};
if !args.is_empty() {
prec = *hashmap.get(&args[0]).unwrap_or_else(|| {
*error = true;
&min_prec
});
}
}
lhs
}
#[inline]
fn setup_hashmap<'a>() -> HashMap<&'a [u8], Precedence> {
let mut hashmap = HashMap::<&'a [u8], Precedence>::new();
hashmap.insert(b"-b", Precedence::UnOp);
hashmap.insert(b"-c", Precedence::UnOp);
hashmap.insert(b"-d", Precedence::UnOp);
hashmap.insert(b"-e", Precedence::UnOp);
hashmap.insert(b"-f", Precedence::UnOp);
hashmap.insert(b"-g", Precedence::UnOp);
hashmap.insert(b"-h", Precedence::UnOp);
hashmap.insert(b"-L", Precedence::UnOp);
hashmap.insert(b"-n", Precedence::UnOp);
hashmap.insert(b"-p", Precedence::UnOp);
hashmap.insert(b"-r", Precedence::UnOp);
hashmap.insert(b"-S", Precedence::UnOp);
hashmap.insert(b"-s", Precedence::UnOp);
hashmap.insert(b"-t", Precedence::UnOp);
hashmap.insert(b"-u", Precedence::UnOp);
hashmap.insert(b"-w", Precedence::UnOp);
hashmap.insert(b"-x", Precedence::UnOp);
hashmap.insert(b"-z", Precedence::UnOp);
hashmap.insert(b"=", Precedence::BinOp);
hashmap.insert(b"!=", Precedence::BinOp);
hashmap.insert(b"-eq", Precedence::BinOp);
hashmap.insert(b"-ne", Precedence::BinOp);
hashmap.insert(b"-gt", Precedence::BinOp);
hashmap.insert(b"-ge", Precedence::BinOp);
hashmap.insert(b"-lt", Precedence::BinOp);
hashmap.insert(b"-le", Precedence::BinOp);
hashmap.insert(b"!", Precedence::BUnOp);
hashmap.insert(b"-a", Precedence::And);
hashmap.insert(b"-o", Precedence::Or);
hashmap.insert(b"(", Precedence::Paren);
hashmap.insert(b")", Precedence::Paren);
hashmap
}
#[derive(Eq, PartialEq)]
enum PathCondition {
BlockSpecial,
@ -334,14 +177,10 @@ enum PathCondition {
}
#[cfg(not(windows))]
fn path(path: &[u8], cond: PathCondition) -> bool {
use std::ffi::OsStr;
fn path(path: &OsStr, cond: PathCondition) -> bool {
use std::fs::{self, Metadata};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
let path = OsStr::from_bytes(path);
const S_ISUID: u32 = 0o4000;
const S_ISGID: u32 = 0o2000;
@ -403,13 +242,14 @@ fn path(path: &[u8], cond: PathCondition) -> bool {
}
#[cfg(windows)]
fn path(path: &[u8], cond: PathCondition) -> bool {
fn path(path: &OsStr, cond: PathCondition) -> bool {
use std::fs::metadata;
let path = from_utf8(path).unwrap();
let stat = match metadata(path) {
Ok(s) => s,
_ => return false,
};
match cond {
PathCondition::BlockSpecial => false,
PathCondition::CharacterSpecial => false,

View file

@ -39,7 +39,6 @@ fn test_double_not_is_false() {
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
fn test_and_not_is_false() {
new_ucmd!().args(&["-a", "!"]).run().status_code(1);
}
@ -51,7 +50,6 @@ fn test_not_and_is_false() {
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 0"]
fn test_not_and_not_succeeds() {
new_ucmd!().args(&["!", "-a", "!"]).succeeds();
}
@ -62,7 +60,6 @@ fn test_simple_or() {
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 0); GNU returns 1"]
fn test_negated_or() {
new_ucmd!()
.args(&["!", "foo", "-o", "bar"])
@ -111,13 +108,8 @@ fn test_solo_paren_is_literal() {
}
#[test]
#[ignore = "GNU considers this an error"]
fn test_solo_empty_parenthetical_is_error() {
new_ucmd!()
.args(&["(", ")"])
.run()
.status_code(2)
.stderr_is("test: missing argument after )");
new_ucmd!().args(&["(", ")"]).run().status_code(2);
}
#[test]
@ -248,7 +240,6 @@ fn test_negative_int_compare() {
}
#[test]
#[ignore = "fixme: error reporting"]
fn test_float_inequality_is_error() {
new_ucmd!()
.args(&["123.45", "-ge", "6"])
@ -257,6 +248,32 @@ fn test_float_inequality_is_error() {
.stderr_is("test: invalid integer 123.45");
}
#[test]
#[cfg(not(windows))]
fn test_invalid_utf8_integer_compare() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let source = [0x66, 0x6f, 0x80, 0x6f];
let arg = OsStr::from_bytes(&source[..]);
let mut cmd = new_ucmd!();
cmd.arg("123").arg("-ne");
cmd.raw.arg(arg);
cmd.run()
.status_code(2)
.stderr_is("test: invalid integer fo<66>o");
let mut cmd = new_ucmd!();
cmd.raw.arg(arg);
cmd.arg("-eq").arg("456");
cmd.run()
.status_code(2)
.stderr_is("test: invalid integer fo<66>o");
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
fn test_file_is_itself() {
@ -445,6 +462,14 @@ fn test_op_prec_and_or_1() {
new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds();
}
#[test]
fn test_op_prec_and_or_1_overridden_by_parentheses() {
new_ucmd!()
.args(&["(", " ", "-o", "", ")", "-a", ""])
.run()
.status_code(1);
}
#[test]
fn test_op_prec_and_or_2() {
new_ucmd!()
@ -452,6 +477,45 @@ fn test_op_prec_and_or_2() {
.succeeds();
}
#[test]
fn test_op_prec_and_or_2_overridden_by_parentheses() {
new_ucmd!()
.args(&["", "-a", "(", "", "-o", " ", ")", "-a", " "])
.run()
.status_code(1);
}
#[test]
#[ignore = "fixme: error reporting"]
fn test_dangling_parenthesis() {
new_ucmd!()
.args(&["(", "(", "a", "!=", "b", ")", "-o", "-n", "c"])
.run()
.status_code(2);
new_ucmd!()
.args(&["(", "(", "a", "!=", "b", ")", "-o", "-n", "c", ")"])
.succeeds();
}
#[test]
fn test_complicated_parenthesized_expression() {
new_ucmd!()
.args(&[
"(", "(", "!", "(", "a", "=", "b", ")", "-o", "c", "=", "d", ")", "-a", "(", "q", "!=",
"r", ")", ")",
])
.succeeds();
}
#[test]
fn test_erroneous_parenthesized_expression() {
new_ucmd!()
.args(&["a", "!=", "(", "b", "-a", "b", ")", "!=", "c"])
.run()
.status_code(2)
.stderr_is("test: extra argument b");
}
#[test]
fn test_or_as_filename() {
new_ucmd!()