use super::prelude::*; use crate::common; use crate::future_feature_flags::{feature_test, FeatureFlag}; use crate::should_flog; mod test_expressions { use super::*; use crate::nix::isatty; use crate::wutil::{ file_id_for_path, fish_wcswidth, lwstat, waccess, wcstod::wcstod, wcstoi_opts, wstat, Error, Options, }; use once_cell::sync::Lazy; use std::collections::HashMap; use std::os::unix::prelude::*; #[derive(Copy, Clone, PartialEq, Eq)] pub(super) enum Token { Unknown, // Arbitrary string Unary(UnaryToken), // Takes one string/file Binary(BinaryToken), // Takes two strings/files/numbers UnaryBoolean(UnaryBooleanToken), // Unary truth function BinaryBoolean(Combiner), // Binary truth function ParenOpen, // ( ParenClose, // ) } impl From for Token { fn from(value: BinaryToken) -> Self { Self::Binary(value) } } impl From for Token { fn from(value: UnaryToken) -> Self { Self::Unary(value) } } #[derive(Copy, Clone, PartialEq, Eq)] pub(super) enum UnaryBooleanToken { Bang, // "!", inverts sense } #[derive(Copy, Clone, PartialEq, Eq)] pub(super) enum Combiner { And, // "-a", true if left and right are both true Or, // "-o", true if either left or right is true } macro_rules! define_token { ( enum $enum:ident; $( $variant:ident($sub_type:ident) { $($sub_variant:ident)+ } )* ) => { #[derive(Copy, Clone, PartialEq, Eq)] pub(super) enum $enum { $($variant($sub_type),)* } $( #[derive(Copy, Clone, PartialEq, Eq)] pub(super) enum $sub_type { $($sub_variant,)+ } impl From<$sub_type> for Token { fn from(value: $sub_type) -> Token { $enum::$variant(value).into() } } )* }; } define_token! { enum UnaryToken; // based on stat() FileStat(StatPredicate) { b // "-b", for block special files c // "-c", for character special files d // "-d", for directories e // "-e", for files that exist f // "-f", for for regular files G // "-G", for check effective group id g // "-g", for set-group-id k // "-k", for sticky bit O // "-O", for check effective user id p // "-p", for FIFO S // "-S", socket s // "-s", size greater than zero u // "-u", whether file is setuid } // based on access() FilePerm(FilePermission) { r // "-r", read permission w // "-w", whether file write permission is allowed x // "-x", whether file execute/search is allowed } // miscellaneous FileType(FilePredicate) { h // "-h", for symbolic links L // "-L", same as -h t // "-t", whether the fd is associated with a terminal } String(StringPredicate) { n // "-n", non-empty string z // "-z", true if length of string is 0 } } define_token! { enum BinaryToken; // based on inode + more distinguishing info (see FileId struct) FileId(FileComparison) { Newer // f1 -nt f2, true if f1 exists and is newer than f2, or there is no f2 Older // f1 -ot f2, true if f2 exists and f1 does not, or f1 is older than f2 Same // f1 -ef f2, true if f1 and f2 exist and refer to same file } String(StringComparison) { Equal // "=", true if strings are identical NotEqual // "!=", true if strings are not identical } Number(NumberComparison) { Equal // "-eq", true if numbers are equal NotEqual // "-ne", true if numbers are not equal Greater // "-gt", true if first number is larger than second GreaterEqual // "-ge", true if first number is at least second Lesser // "-lt", true if first number is smaller than second LesserEqual // "-le", true if first number is at most second } } /// Our number type. We support both doubles and long longs. We have to support these separately /// because some integers are not representable as doubles; these may come up in practice (e.g. /// inodes). #[derive(Copy, Clone, Default, PartialEq, PartialOrd)] struct Number { // A number has an integral base and a floating point delta. // Conceptually the number is base + delta. // We enforce the property that 0 <= delta < 1. base: i64, delta: f64, } impl Number { pub(super) fn new(base: i64, delta: f64) -> Self { assert!((0.0..1.0).contains(&delta), "Invalid delta"); Self { base, delta } } // Return true if the number is a tty(). fn isatty(&self, streams: &mut IoStreams) -> bool { if self.delta != 0.0 || self.base > i32::MAX as i64 || self.base < i32::MIN as i64 { return false; } let bint = self.base as i32; if bint == 0 { match streams.stdin_fd { -1 => false, fd => isatty(fd), } } else if bint == 1 { !streams.out_is_redirected && isatty(libc::STDOUT_FILENO) } else if bint == 2 { !streams.err_is_redirected && isatty(libc::STDERR_FILENO) } else { isatty(bint) } } } fn token_for_string(str: &wstr) -> Token { TOKEN_INFOS.get(str).copied().unwrap_or(Token::Unknown) } static TOKEN_INFOS: Lazy> = Lazy::new(|| { let pairs = [ (L!(""), Token::Unknown), (L!("!"), Token::UnaryBoolean(UnaryBooleanToken::Bang)), (L!("-b"), StatPredicate::b.into()), (L!("-c"), StatPredicate::c.into()), (L!("-d"), StatPredicate::d.into()), (L!("-e"), StatPredicate::e.into()), (L!("-f"), StatPredicate::f.into()), (L!("-G"), StatPredicate::G.into()), (L!("-g"), StatPredicate::g.into()), (L!("-h"), FilePredicate::h.into()), (L!("-k"), StatPredicate::k.into()), (L!("-L"), FilePredicate::L.into()), (L!("-O"), StatPredicate::O.into()), (L!("-p"), StatPredicate::p.into()), (L!("-S"), StatPredicate::S.into()), (L!("-s"), StatPredicate::s.into()), (L!("-t"), FilePredicate::t.into()), (L!("-r"), FilePermission::r.into()), (L!("-u"), StatPredicate::u.into()), (L!("-w"), FilePermission::w.into()), (L!("-x"), FilePermission::x.into()), (L!("-n"), StringPredicate::n.into()), (L!("-z"), StringPredicate::z.into()), (L!("="), StringComparison::Equal.into()), (L!("!="), StringComparison::NotEqual.into()), (L!("-nt"), FileComparison::Newer.into()), (L!("-ot"), FileComparison::Older.into()), (L!("-ef"), FileComparison::Same.into()), (L!("-eq"), NumberComparison::Equal.into()), (L!("-ne"), NumberComparison::NotEqual.into()), (L!("-gt"), NumberComparison::Greater.into()), (L!("-ge"), NumberComparison::GreaterEqual.into()), (L!("-lt"), NumberComparison::Lesser.into()), (L!("-le"), NumberComparison::LesserEqual.into()), (L!("-a"), Token::BinaryBoolean(Combiner::And)), (L!("-o"), Token::BinaryBoolean(Combiner::Or)), (L!("("), Token::ParenOpen), (L!(")"), Token::ParenClose), ]; pairs.into_iter().collect() }); // Grammar. // // = // // = and/or | // // // = bang | // // // = arg | // arg arg | // '(' ')' #[derive(Default)] pub(super) struct TestParser<'a> { strings: &'a [WString], errors: Vec, error_idx: usize, } impl<'a> TestParser<'a> { fn arg(&self, idx: usize) -> &'a wstr { &self.strings[idx] } fn add_error(&mut self, idx: usize, text: WString) { self.errors.push(text); if self.errors.len() == 1 { self.error_idx = idx; } } } type Range = std::ops::Range; /// Base trait for expressions. pub(super) trait Expression { /// Evaluate returns true if the expression is true (i.e. STATUS_CMD_OK). fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec) -> bool; /// Return base.range. fn range(&self) -> Range; // Helper to convert ourselves into Some Box. fn into_some_box(self) -> Option> where Self: Sized + 'static, { Some(Box::new(self)) } } /// Something that is not a token of any other type. struct JustAString { arg: WString, range: Range, } /// Single argument like -n foo. struct UnaryPrimary { arg: WString, token: UnaryToken, range: Range, } /// Two argument primary like foo != bar. struct BinaryPrimary { arg_left: WString, arg_right: WString, token: BinaryToken, range: Range, } /// Unary operator like bang. struct UnaryOperator { subject: Box, token: UnaryBooleanToken, range: Range, } /// Combining expression. Contains a list of AND or OR expressions. It takes more than two so that /// we don't have to worry about precedence in the parser. struct CombiningExpression { subjects: Vec>, combiners: Vec, range: Range, } /// Parenthentical expression. struct ParentheticalExpression { contents: Box, range: Range, } impl Expression for JustAString { fn evaluate(&self, _streams: &mut IoStreams, _errors: &mut Vec) -> bool { !self.arg.is_empty() } fn range(&self) -> Range { self.range.clone() } } impl Expression for UnaryPrimary { fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec) -> bool { unary_primary_evaluate(self.token, &self.arg, streams, errors) } fn range(&self) -> Range { self.range.clone() } } impl Expression for BinaryPrimary { fn evaluate(&self, _streams: &mut IoStreams, errors: &mut Vec) -> bool { binary_primary_evaluate(self.token, &self.arg_left, &self.arg_right, errors) } fn range(&self) -> Range { self.range.clone() } } impl Expression for UnaryOperator { fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec) -> bool { match self.token { UnaryBooleanToken::Bang => !self.subject.evaluate(streams, errors), } } fn range(&self) -> Range { self.range.clone() } } impl Expression for CombiningExpression { fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec) -> bool { let _res = self.subjects[0].evaluate(streams, errors); assert!(!self.subjects.is_empty()); assert!(self.combiners.len() + 1 == self.subjects.len()); // One-element case. if self.subjects.len() == 1 { return self.subjects[0].evaluate(streams, errors); } // Evaluate our lists, remembering that AND has higher precedence than OR. We can // visualize this as a sequence of OR expressions of AND expressions. let mut idx = 0; let max = self.subjects.len(); let mut or_result = false; while idx < max { if or_result { // short circuit break; } // Evaluate a stream of AND starting at given subject index. It may only have one // element. let mut and_result = true; while idx < max { // Evaluate it, short-circuiting. and_result = and_result && self.subjects[idx].evaluate(streams, errors); // If the combiner at this index (which corresponding to how we combine with the // next subject) is not AND, then exit the loop. if idx + 1 < max && self.combiners[idx] != Combiner::And { idx += 1; break; } idx += 1; } // OR it in. or_result = or_result || and_result; } return or_result; } fn range(&self) -> Range { self.range.clone() } } impl Expression for ParentheticalExpression { fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec) -> bool { self.contents.evaluate(streams, errors) } fn range(&self) -> Range { self.range.clone() } } impl<'a> TestParser<'a> { fn error(&mut self, idx: usize, text: WString) -> Option> { self.add_error(idx, text); None } fn parse_unary_expression( &mut self, start: usize, end: usize, ) -> Option> { if start >= end { return self.error(start, sprintf!("Missing argument at index %u", start + 1)); } if let Token::UnaryBoolean(token) = token_for_string(self.arg(start)) { let subject = self.parse_unary_expression(start + 1, end)?; let range = start..subject.range().end; return UnaryOperator { subject, token, range, } .into_some_box(); } self.parse_primary(start, end) } /// Parse a combining expression (AND, OR). fn parse_combining_expression( &mut self, start: usize, end: usize, ) -> Option> { if start >= end { return None; } let mut subjects = Vec::new(); let mut combiners = Vec::new(); let mut idx = start; let mut first = true; while idx < end { if !first { // This is not the first expression, so we expect a combiner. let Token::BinaryBoolean(combiner) = token_for_string(self.arg(idx)) else { /* Not a combiner, we're done */ self.errors.insert( 0, sprintf!( "Expected a combining operator like '-a' at index %u", idx + 1 ), ); self.error_idx = idx; break; }; combiners.push(combiner); idx += 1; } // Parse another expression. let Some(expr) = self.parse_unary_expression(idx, end) else { self.add_error(idx, sprintf!("Missing argument at index %u", idx + 1)); if !first { // Clean up the dangling combiner, since it never got its right hand expression. combiners.pop(); } break; }; // Go to the end of this expression. idx = expr.range().end; subjects.push(expr); first = false; } if subjects.is_empty() { return None; // no subjects } // Our new expression takes ownership of all expressions we created. The base token we pass is // irrelevant. CombiningExpression { subjects, combiners, range: start..idx, } .into_some_box() } fn parse_unary_primary(&mut self, start: usize, end: usize) -> Option> { // We need two arguments. if start >= end { return self.error(start, sprintf!("Missing argument at index %u", start + 1)); } if start + 1 >= end { return self.error( start + 1, sprintf!("Missing argument at index %u", start + 2), ); } // All our unary primaries are prefix, so the operator is at start. let Token::Unary(token) = token_for_string(self.arg(start)) else { return None; }; UnaryPrimary { arg: self.arg(start + 1).to_owned(), token, range: start..start + 2, } .into_some_box() } fn parse_just_a_string(&mut self, start: usize, end: usize) -> Option> { // Handle a string as a unary primary that is not a token of any other type. // e.g. 'test foo -a bar' should evaluate to true. // We need one argument. if start >= end { return self.error(start, sprintf!("Missing argument at index %u", start + 1)); } let tok = token_for_string(self.arg(start)); if tok != Token::Unknown { return self.error( start, sprintf!("Unexpected argument type at index %u", start + 1), ); } if feature_test(FeatureFlag::test_require_arg) { return self.error(start, sprintf!("Unknown option at index %u", start)); } return JustAString { arg: self.arg(start).to_owned(), range: start..start + 1, } .into_some_box(); } fn parse_binary_primary( &mut self, start: usize, end: usize, ) -> Option> { // We need three arguments. for idx in start..start + 3 { if idx >= end { return self.error(idx, sprintf!("Missing argument at index %u", idx + 1)); } } // All our binary primaries are infix, so the operator is at start + 1. let Token::Binary(token) = token_for_string(self.arg(start + 1)) else { return None; }; BinaryPrimary { arg_left: self.arg(start).to_owned(), arg_right: self.arg(start + 2).to_owned(), token, range: start..start + 3, } .into_some_box() } fn parse_parenthetical(&mut self, start: usize, end: usize) -> Option> { // We need at least three arguments: open paren, argument, close paren. if start + 3 >= end { return None; } // Must start with an open expression. if token_for_string(self.arg(start)) != Token::ParenOpen { return None; } // Parse a subexpression. let subexpr = self.parse_expression(start + 1, end)?; // Parse a close paren. let close_index = subexpr.range().end; assert!(close_index <= end); if close_index == end { return self.error( close_index, sprintf!("Missing close paren at index %u", close_index + 1), ); } if token_for_string(self.arg(close_index)) != Token::ParenClose { return self.error( close_index, sprintf!("Expected close paren at index %u", close_index + 1), ); } // Success. ParentheticalExpression { contents: subexpr, range: start..close_index + 1, } .into_some_box() } fn parse_primary(&mut self, start: usize, end: usize) -> Option> { if start >= end { return self.error(start, sprintf!("Missing argument at index %u", start + 1)); } let mut expr = None; if expr.is_none() { expr = self.parse_parenthetical(start, end); } if expr.is_none() { expr = self.parse_unary_primary(start, end); } if expr.is_none() { expr = self.parse_binary_primary(start, end); } if expr.is_none() { expr = self.parse_just_a_string(start, end); } expr } // See IEEE 1003.1 breakdown of the behavior for different parameter counts. fn parse_3_arg_expression( &mut self, start: usize, end: usize, ) -> Option> { assert!(end - start == 3); let center_token = token_for_string(self.arg(start + 1)); if matches!(center_token, Token::Binary(_)) { self.parse_binary_primary(start, end) } else if let Token::BinaryBoolean(combiner) = center_token { let left = self.parse_unary_expression(start, start + 1)?; let right = self.parse_unary_expression(start + 2, start + 3)?; // Transfer ownership to the vector of subjects. CombiningExpression { subjects: vec![left, right], combiners: vec![combiner], range: start..end, } .into_some_box() } else { self.parse_unary_expression(start, end) } } fn parse_4_arg_expression( &mut self, start: usize, end: usize, ) -> Option> { assert!(end - start == 4); let first_token = token_for_string(self.arg(start)); if let Token::UnaryBoolean(token) = first_token { let subject = self.parse_3_arg_expression(start + 1, end)?; UnaryOperator { subject, token, range: start..end, } .into_some_box() } else if first_token == Token::ParenOpen { self.parse_parenthetical(start, end) } else { self.parse_combining_expression(start, end) } } fn parse_expression(&mut self, start: usize, end: usize) -> Option> { if start >= end { return self.error(start, sprintf!("Missing argument at index %u", start + 1)); } let argc = end - start; match argc { 0 => { panic!("argc should not be zero"); // should have been caught by the above test } 1 => self.error( start + 1, sprintf!("Missing argument at index %u", start + 2), ), 2 => self.parse_unary_expression(start, end), 3 => self.parse_3_arg_expression(start, end), 4 => self.parse_4_arg_expression(start, end), _ => self.parse_combining_expression(start, end), } } pub fn parse_args( args: &[WString], err: &mut WString, program_name: &wstr, ) -> Option> { let mut parser = TestParser { strings: args, errors: Vec::new(), error_idx: 0, }; let mut result = parser.parse_expression(0, args.len()); // Historic assumption from C++: if we have no errors then we must have a result. assert!(!parser.errors.is_empty() || result.is_some()); // Handle errors. // For now we only show the first error. if !parser.errors.is_empty() || result.as_ref().unwrap().range().end < args.len() { let mut narg = 0; let mut len_to_err = 0; if parser.errors.is_empty() { parser.error_idx = result.as_ref().unwrap().range().end; } let mut commandline = WString::new(); for arg in args { if narg > 0 { commandline.push(' '); } commandline.push_utfstr(arg); narg += 1; if narg == parser.error_idx { len_to_err = fish_wcswidth(&commandline); } } err.push_utfstr(program_name); err.push_str(": "); if !parser.errors.is_empty() { err.push_utfstr(&parser.errors[0]); } else { sprintf!(=> err, "unexpected argument at index %lu: '%ls'", result.as_ref().unwrap().range().end + 1, args[result.as_ref().unwrap().range().end]); } err.push('\n'); err.push_utfstr(&commandline); err.push('\n'); err.push_utfstr(&sprintf!("%*ls%ls\n", len_to_err + 1, " ", "^")); } if result.is_some() { // It's also an error if there are any unused arguments. This is not detected by // parse_expression(). assert!(result.as_ref().unwrap().range().end <= args.len()); if result.as_ref().unwrap().range().end < args.len() { result = None; } } result } } // Parse a double from arg. fn parse_double(argstr: &wstr) -> Result { let mut arg = argstr; // Consume leading spaces. while !arg.is_empty() && arg.char_at(0).is_whitespace() { arg = arg.slice_from(1); } if arg.is_empty() { return Err(Error::Empty); } let mut consumed = 0; let res = wcstod(arg, '.', &mut consumed)?; // Consume trailing spaces. let mut end = arg.slice_from(consumed); while !end.is_empty() && end.char_at(0).is_whitespace() { end = end.slice_from(1); } if end.len() < argstr.len() && end.is_empty() { Ok(res) } else { Err(Error::InvalidChar) } } // IEEE 1003.1 says nothing about what it means for two strings to be "algebraically equal". For // example, should we interpret 0x10 as 0, 10, or 16? Here we use only base 10 and use wcstoll, // which allows for leading + and -, and whitespace. This is consistent, albeit a bit more lenient // since we allow trailing whitespace, with other implementations such as bash. fn parse_number(arg: &wstr, number: &mut Number, errors: &mut Vec) -> bool { let floating = parse_double(arg); let integral: Result = fish_wcstol(arg); if let Ok(int) = integral { // Here the value is just an integer; ignore the floating point parse because it may be // invalid (e.g. not a representable integer). *number = Number::new(int, 0.0); true } else if floating.is_ok_and(|f| f.is_finite()) && integral.is_err_and(|i| i != Error::Overflow) { // Here we parsed an (in range) floating point value that could not be parsed as an integer. // Break the floating point value into base and delta. Ensure that base is <= the floating // point value. // // Note that a non-finite number like infinity or NaN doesn't work for us, so we checked // above. let floating = floating.unwrap(); let intpart = floating.floor(); let delta = floating - intpart; *number = Number::new(intpart as i64, delta); true } else { // We could not parse a float or an int. // Check for special fish_wcsto* value or show standard EINVAL/ERANGE error. // TODO: the C++ here was pretty confusing. In particular we used an errno of -1 to mean // "invalid char" but the input string may be something like "inf". if integral == Err(Error::InvalidChar) && floating.is_err() { // Historically fish has printed a special message if a prefix of the invalid string was an integer. // Compute that now. let options = Options { mradix: Some(10), ..Default::default() }; if let Ok(prefix_int) = wcstoi_opts(arg, options) { let _: i64 = prefix_int; // to help type inference errors.push(wgettext_fmt!( "Integer %lld in '%ls' followed by non-digit", prefix_int, arg )); } else { errors.push(wgettext_fmt!("Argument is not a number: '%ls'", arg)); } } else if floating.map_or(false, |x| x.is_nan()) { // NaN is an error as far as we're concerned. errors.push(wgettext!("Not a number").to_owned()); } else if floating.map_or(false, |x| x.is_infinite()) { errors.push(wgettext!("Number is infinite").to_owned()); } else if integral == Err(Error::Overflow) { errors.push(wgettext_fmt!("Result too large: %ls", arg)); } else { errors.push(wgettext_fmt!("Invalid number: %ls", arg)); } false } } fn binary_primary_evaluate( token: BinaryToken, left: &wstr, right: &wstr, errors: &mut Vec, ) -> bool { match token { BinaryToken::String(StringComparison::Equal) => left == right, BinaryToken::String(StringComparison::NotEqual) => left != right, BinaryToken::FileId(comparison) => { let left = file_id_for_path(left); let right = file_id_for_path(right); match comparison { FileComparison::Newer => right.older_than(&left), FileComparison::Older => left.older_than(&right), FileComparison::Same => left == right, } } BinaryToken::Number(comparison) => { let mut ln = Number::default(); let mut rn = Number::default(); if !parse_number(left, &mut ln, errors) || !parse_number(right, &mut rn, errors) { return false; } match comparison { NumberComparison::Equal => ln == rn, NumberComparison::NotEqual => ln != rn, NumberComparison::Greater => ln > rn, NumberComparison::GreaterEqual => ln >= rn, NumberComparison::Lesser => ln < rn, NumberComparison::LesserEqual => ln <= rn, } } } } fn unary_primary_evaluate( token: UnaryToken, arg: &wstr, streams: &mut IoStreams, errors: &mut Vec, ) -> bool { match token { #[allow(clippy::unnecessary_cast)] // mode_t is u32 on many platforms, but not all UnaryToken::FileStat(stat_token) => { let Ok(md) = wstat(arg) else { return false; }; const S_ISUID: u32 = libc::S_ISUID as u32; const S_ISGID: u32 = libc::S_ISGID as u32; const S_ISVTX: u32 = libc::S_ISVTX as u32; match stat_token { // "-b", for block special files StatPredicate::b => md.file_type().is_block_device(), // "-c", for character special files StatPredicate::c => md.file_type().is_char_device(), // "-d", for directories StatPredicate::d => md.file_type().is_dir(), // "-e", for files that exist StatPredicate::e => true, // "-f", for regular files StatPredicate::f => md.file_type().is_file(), // "-G", for check effective group id StatPredicate::G => md.gid() == crate::nix::getegid(), // "-g", for set-group-id StatPredicate::g => md.permissions().mode() & S_ISGID != 0, // "-k", for sticky bit StatPredicate::k => md.permissions().mode() & S_ISVTX != 0, // "-O", for check effective user id StatPredicate::O => md.uid() == crate::nix::geteuid(), // "-p", for FIFO StatPredicate::p => md.file_type().is_fifo(), // "-S", socket StatPredicate::S => md.file_type().is_socket(), // "-s", size greater than zero StatPredicate::s => md.len() > 0, // "-u", whether file is setuid StatPredicate::u => md.permissions().mode() & S_ISUID != 0, } } UnaryToken::FileType(file_type) => { match file_type { // "-h", for symbolic links // "-L", same as -h FilePredicate::h | FilePredicate::L => { lwstat(arg).is_ok_and(|md| md.file_type().is_symlink()) } // "-t", whether the fd is associated with a terminal FilePredicate::t => { let mut num = Number::default(); parse_number(arg, &mut num, errors) && num.isatty(streams) } } } UnaryToken::FilePerm(permission) => { let mode = match permission { // "-r", read permission FilePermission::r => libc::R_OK, // "-w", whether file write permission is allowed FilePermission::w => libc::W_OK, // "-x", whether file execute/search is allowed FilePermission::x => libc::X_OK, }; waccess(arg, mode) == 0 } UnaryToken::String(predicate) => match predicate { // "-n", non-empty string StringPredicate::n => !arg.is_empty(), // "-z", true if length of string is 0 StringPredicate::z => arg.is_empty(), }, } } } /// Evaluate a conditional expression given the arguments. For POSIX conformance this /// supports a more limited range of functionality. /// Return status is the final shell status, i.e. 0 for true, 1 for false and 2 for error. pub fn test(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { // The first argument should be the name of the command ('test'). if argv.is_empty() { return STATUS_INVALID_ARGS; } // Whether we are invoked with bracket '[' or not. let program_name = argv[0]; let is_bracket = program_name == "["; let mut argc = argv.len() - 1; // If we're bracket, the last argument ought to be ]; we ignore it. Note that argc is the number // of arguments after the command name; thus argv[argc] is the last argument. if is_bracket { if argv[argc] == "]" { // Ignore the closing bracket from now on. argc -= 1; } else { streams .err .appendln(wgettext!("[: the last argument must be ']'")); builtin_print_error_trailer(parser, streams.err, program_name); return STATUS_INVALID_ARGS; } } // Collect the arguments into a list. let args: Vec = argv[1..argc + 1] .iter() .map(|&arg| arg.to_owned()) .collect(); let args: &[WString] = &args; if feature_test(FeatureFlag::test_require_arg) { if argc == 0 { streams.err.appendln(wgettext_fmt!( "%ls: Expected at least one argument", program_name )); builtin_print_error_trailer(parser, streams.err, program_name); return STATUS_INVALID_ARGS; } else if argc == 1 { if args[0] == "-n" { return STATUS_CMD_ERROR; } else if args[0] == "-z" { return STATUS_CMD_OK; } } } else if argc == 0 { if should_flog!(deprecated_test) { streams.err.appendln(wgettext_fmt!( "%ls: called with no arguments. This will be an error in future.", program_name )); streams.err.append(parser.current_line()); } return STATUS_INVALID_ARGS; // Per 1003.1, exit false. } else if argc == 1 { if should_flog!(deprecated_test) { if args[0] != "-z" { streams.err.appendln(wgettext_fmt!( "%ls: called with one argument. This will return false in future.", program_name )); streams.err.append(parser.current_line()); } } // Per 1003.1, exit true if the arg is non-empty. return if args[0].is_empty() { STATUS_CMD_ERROR } else { STATUS_CMD_OK }; } // Try parsing let mut err = WString::new(); let expr = test_expressions::TestParser::parse_args(args, &mut err, program_name); let Some(expr) = expr else { streams.err.append(err); streams.err.append(parser.current_line()); return STATUS_CMD_ERROR; }; let mut eval_errors = Vec::new(); let result = expr.evaluate(streams, &mut eval_errors); if !eval_errors.is_empty() { if !common::should_suppress_stderr_for_tests() { for eval_error in eval_errors { streams.err.appendln(&eval_error); } // Add a backtrace but not the "see help" message // because this isn't about passing the wrong options. streams.err.append(parser.current_line()); } return STATUS_INVALID_ARGS; } if result { STATUS_CMD_OK } else { STATUS_CMD_ERROR } }