2023-08-09 06:17:10 +08:00
|
|
|
use super::prelude::*;
|
2023-05-15 05:48:17 +08:00
|
|
|
use crate::common;
|
2024-04-21 20:25:54 +08:00
|
|
|
use crate::future_feature_flags::{feature_test, FeatureFlag};
|
|
|
|
use crate::should_flog;
|
2023-05-15 05:48:17 +08:00
|
|
|
|
|
|
|
mod test_expressions {
|
2023-08-09 06:16:04 +08:00
|
|
|
use super::*;
|
2023-08-09 06:17:10 +08:00
|
|
|
|
2023-11-20 03:07:24 +08:00
|
|
|
use crate::nix::isatty;
|
2023-05-15 05:48:17 +08:00
|
|
|
use crate::wutil::{
|
2023-08-09 06:17:10 +08:00
|
|
|
file_id_for_path, fish_wcswidth, lwstat, waccess, wcstod::wcstod, wcstoi_opts, wstat,
|
|
|
|
Error, Options,
|
2023-05-15 05:48:17 +08:00
|
|
|
};
|
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
use std::collections::HashMap;
|
2023-08-06 20:56:30 +08:00
|
|
|
use std::os::unix::prelude::*;
|
2023-05-15 05:48:17 +08:00
|
|
|
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
|
|
pub(super) enum Token {
|
2024-03-16 12:24:44 +08:00
|
|
|
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<BinaryToken> for Token {
|
|
|
|
fn from(value: BinaryToken) -> Self {
|
|
|
|
Self::Binary(value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<UnaryToken> 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,)+ }
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
impl From<$sub_type> for Token {
|
|
|
|
fn from(value: $sub_type) -> Token {
|
|
|
|
$enum::$variant(value).into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)*
|
|
|
|
};
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
define_token! {
|
|
|
|
enum UnaryToken;
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
// 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
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
// 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
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
// miscellaneous
|
|
|
|
FileType(FilePredicate) {
|
|
|
|
h // "-h", for symbolic links
|
|
|
|
L // "-L", same as -h
|
|
|
|
t // "-t", whether the fd is associated with a terminal
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
String(StringPredicate) {
|
|
|
|
n // "-n", non-empty string
|
|
|
|
z // "-z", true if length of string is 0
|
|
|
|
}
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
define_token! {
|
|
|
|
enum BinaryToken;
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
// 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
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
String(StringComparison) {
|
|
|
|
Equal // "=", true if strings are identical
|
|
|
|
NotEqual // "!=", true if strings are not identical
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
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
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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().
|
2023-10-09 05:22:27 +08:00
|
|
|
fn isatty(&self, streams: &mut IoStreams) -> bool {
|
2023-05-15 05:48:17 +08:00
|
|
|
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 {
|
2023-10-09 05:22:27 +08:00
|
|
|
match streams.stdin_fd {
|
|
|
|
-1 => false,
|
2023-11-20 03:07:24 +08:00
|
|
|
fd => isatty(fd),
|
2023-10-09 05:22:27 +08:00
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
} else if bint == 1 {
|
2023-11-20 03:07:24 +08:00
|
|
|
!streams.out_is_redirected && isatty(libc::STDOUT_FILENO)
|
2023-05-15 05:48:17 +08:00
|
|
|
} else if bint == 2 {
|
2023-11-20 03:07:24 +08:00
|
|
|
!streams.err_is_redirected && isatty(libc::STDERR_FILENO)
|
2023-05-15 05:48:17 +08:00
|
|
|
} else {
|
2023-11-20 03:07:24 +08:00
|
|
|
isatty(bint)
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
fn token_for_string(str: &wstr) -> Token {
|
|
|
|
TOKEN_INFOS.get(str).copied().unwrap_or(Token::Unknown)
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
static TOKEN_INFOS: Lazy<HashMap<&'static wstr, Token>> = Lazy::new(|| {
|
2023-05-15 05:48:17 +08:00
|
|
|
let pairs = [
|
2024-03-16 12:24:44 +08:00
|
|
|
(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),
|
2023-05-15 05:48:17 +08:00
|
|
|
];
|
|
|
|
pairs.into_iter().collect()
|
|
|
|
});
|
|
|
|
|
|
|
|
// Grammar.
|
|
|
|
//
|
|
|
|
// <expr> = <combining_expr>
|
|
|
|
//
|
|
|
|
// <combining_expr> = <unary_expr> and/or <combining_expr> |
|
|
|
|
// <unary_expr>
|
|
|
|
//
|
|
|
|
// <unary_expr> = bang <unary_expr> |
|
|
|
|
// <primary>
|
|
|
|
//
|
|
|
|
// <primary> = <unary_primary> arg |
|
|
|
|
// arg <binary_primary> arg |
|
|
|
|
// '(' <expr> ')'
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
pub(super) struct TestParser<'a> {
|
|
|
|
strings: &'a [WString],
|
|
|
|
errors: Vec<WString>,
|
|
|
|
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<usize>;
|
|
|
|
|
|
|
|
/// Base trait for expressions.
|
|
|
|
pub(super) trait Expression {
|
|
|
|
/// Evaluate returns true if the expression is true (i.e. STATUS_CMD_OK).
|
2023-10-09 05:10:05 +08:00
|
|
|
fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec<WString>) -> bool;
|
2023-05-15 05:48:17 +08:00
|
|
|
|
|
|
|
/// Return base.range.
|
|
|
|
fn range(&self) -> Range;
|
|
|
|
|
|
|
|
// Helper to convert ourselves into Some Box.
|
|
|
|
fn into_some_box(self) -> Option<Box<dyn Expression>>
|
|
|
|
where
|
|
|
|
Self: Sized + 'static,
|
|
|
|
{
|
|
|
|
Some(Box::new(self))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
/// Something that is not a token of any other type.
|
|
|
|
struct JustAString {
|
|
|
|
arg: WString,
|
|
|
|
range: Range,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Single argument like -n foo.
|
2023-05-15 05:48:17 +08:00
|
|
|
struct UnaryPrimary {
|
|
|
|
arg: WString,
|
2024-03-16 12:24:44 +08:00
|
|
|
token: UnaryToken,
|
2023-05-15 05:48:17 +08:00
|
|
|
range: Range,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Two argument primary like foo != bar.
|
|
|
|
struct BinaryPrimary {
|
|
|
|
arg_left: WString,
|
|
|
|
arg_right: WString,
|
2024-03-16 12:24:44 +08:00
|
|
|
token: BinaryToken,
|
2023-05-15 05:48:17 +08:00
|
|
|
range: Range,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Unary operator like bang.
|
|
|
|
struct UnaryOperator {
|
|
|
|
subject: Box<dyn Expression>,
|
2024-03-16 12:24:44 +08:00
|
|
|
token: UnaryBooleanToken,
|
2023-05-15 05:48:17 +08:00
|
|
|
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<Box<dyn Expression>>,
|
2024-03-16 12:24:44 +08:00
|
|
|
combiners: Vec<Combiner>,
|
2023-05-15 05:48:17 +08:00
|
|
|
range: Range,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parenthentical expression.
|
|
|
|
struct ParentheticalExpression {
|
|
|
|
contents: Box<dyn Expression>,
|
|
|
|
range: Range,
|
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
impl Expression for JustAString {
|
|
|
|
fn evaluate(&self, _streams: &mut IoStreams, _errors: &mut Vec<WString>) -> bool {
|
|
|
|
!self.arg.is_empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn range(&self) -> Range {
|
|
|
|
self.range.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-15 05:48:17 +08:00
|
|
|
impl Expression for UnaryPrimary {
|
2023-10-09 05:10:05 +08:00
|
|
|
fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec<WString>) -> bool {
|
2023-05-15 05:48:17 +08:00
|
|
|
unary_primary_evaluate(self.token, &self.arg, streams, errors)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn range(&self) -> Range {
|
|
|
|
self.range.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Expression for BinaryPrimary {
|
2023-10-09 05:10:05 +08:00
|
|
|
fn evaluate(&self, _streams: &mut IoStreams, errors: &mut Vec<WString>) -> bool {
|
2023-05-15 05:48:17 +08:00
|
|
|
binary_primary_evaluate(self.token, &self.arg_left, &self.arg_right, errors)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn range(&self) -> Range {
|
|
|
|
self.range.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Expression for UnaryOperator {
|
2023-10-09 05:10:05 +08:00
|
|
|
fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec<WString>) -> bool {
|
2024-03-16 12:24:44 +08:00
|
|
|
match self.token {
|
|
|
|
UnaryBooleanToken::Bang => !self.subject.evaluate(streams, errors),
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn range(&self) -> Range {
|
|
|
|
self.range.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Expression for CombiningExpression {
|
2023-10-09 05:10:05 +08:00
|
|
|
fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec<WString>) -> bool {
|
2023-05-15 05:48:17 +08:00
|
|
|
let _res = self.subjects[0].evaluate(streams, errors);
|
2024-03-16 12:24:44 +08:00
|
|
|
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;
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
// Evaluate a stream of AND starting at given subject index. It may only have one
|
|
|
|
// element.
|
|
|
|
let mut and_result = true;
|
2023-05-15 05:48:17 +08:00
|
|
|
while idx < max {
|
2024-03-16 12:24:44 +08:00
|
|
|
// Evaluate it, short-circuiting.
|
|
|
|
and_result = and_result && self.subjects[idx].evaluate(streams, errors);
|
2023-05-15 05:48:17 +08:00
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
// 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 {
|
2023-05-15 05:48:17 +08:00
|
|
|
idx += 1;
|
2024-03-16 12:24:44 +08:00
|
|
|
break;
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
idx += 1;
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
|
|
|
|
// OR it in.
|
|
|
|
or_result = or_result || and_result;
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
return or_result;
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn range(&self) -> Range {
|
|
|
|
self.range.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Expression for ParentheticalExpression {
|
2023-10-09 05:10:05 +08:00
|
|
|
fn evaluate(&self, streams: &mut IoStreams, errors: &mut Vec<WString>) -> bool {
|
2023-05-15 05:48:17 +08:00
|
|
|
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<Box<dyn Expression>> {
|
|
|
|
self.add_error(idx, text);
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_unary_expression(
|
|
|
|
&mut self,
|
|
|
|
start: usize,
|
|
|
|
end: usize,
|
|
|
|
) -> Option<Box<dyn Expression>> {
|
|
|
|
if start >= end {
|
|
|
|
return self.error(start, sprintf!("Missing argument at index %u", start + 1));
|
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
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,
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
.into_some_box();
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
self.parse_primary(start, end)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parse a combining expression (AND, OR).
|
|
|
|
fn parse_combining_expression(
|
|
|
|
&mut self,
|
|
|
|
start: usize,
|
|
|
|
end: usize,
|
|
|
|
) -> Option<Box<dyn Expression>> {
|
|
|
|
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.
|
2024-03-16 12:24:44 +08:00
|
|
|
let Token::BinaryBoolean(combiner) = token_for_string(self.arg(idx)) else {
|
2023-05-15 05:48:17 +08:00
|
|
|
/* 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;
|
2024-03-16 12:24:44 +08:00
|
|
|
};
|
2023-05-15 05:48:17 +08:00
|
|
|
combiners.push(combiner);
|
|
|
|
idx += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse another expression.
|
2024-01-27 19:05:47 +08:00
|
|
|
let Some(expr) = self.parse_unary_expression(idx, end) else {
|
2023-05-15 05:48:17 +08:00
|
|
|
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;
|
2024-01-27 19:05:47 +08:00
|
|
|
};
|
2023-05-15 05:48:17 +08:00
|
|
|
// 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<Box<dyn Expression>> {
|
|
|
|
// 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.
|
2024-03-16 12:24:44 +08:00
|
|
|
let Token::Unary(token) = token_for_string(self.arg(start)) else {
|
2023-05-15 05:48:17 +08:00
|
|
|
return None;
|
2024-03-16 12:24:44 +08:00
|
|
|
};
|
2023-05-15 05:48:17 +08:00
|
|
|
UnaryPrimary {
|
|
|
|
arg: self.arg(start + 1).to_owned(),
|
2024-03-16 12:24:44 +08:00
|
|
|
token,
|
2023-05-15 05:48:17 +08:00
|
|
|
range: start..start + 2,
|
|
|
|
}
|
|
|
|
.into_some_box()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_just_a_string(&mut self, start: usize, end: usize) -> Option<Box<dyn Expression>> {
|
2024-03-16 12:24:44 +08:00
|
|
|
// 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.
|
2023-05-15 05:48:17 +08:00
|
|
|
|
|
|
|
// We need one argument.
|
|
|
|
if start >= end {
|
|
|
|
return self.error(start, sprintf!("Missing argument at index %u", start + 1));
|
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
let tok = token_for_string(self.arg(start));
|
|
|
|
if tok != Token::Unknown {
|
2023-05-15 05:48:17 +08:00
|
|
|
return self.error(
|
|
|
|
start,
|
|
|
|
sprintf!("Unexpected argument type at index %u", start + 1),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-21 20:25:54 +08:00
|
|
|
if feature_test(FeatureFlag::test_require_arg) {
|
|
|
|
return self.error(start, sprintf!("Unknown option at index %u", start));
|
|
|
|
}
|
|
|
|
|
2024-03-16 12:24:44 +08:00
|
|
|
return JustAString {
|
2023-05-15 05:48:17 +08:00
|
|
|
arg: self.arg(start).to_owned(),
|
|
|
|
range: start..start + 1,
|
|
|
|
}
|
|
|
|
.into_some_box();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_binary_primary(
|
|
|
|
&mut self,
|
|
|
|
start: usize,
|
|
|
|
end: usize,
|
|
|
|
) -> Option<Box<dyn Expression>> {
|
|
|
|
// 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.
|
2024-03-16 12:24:44 +08:00
|
|
|
let Token::Binary(token) = token_for_string(self.arg(start + 1)) else {
|
2023-05-15 05:48:17 +08:00
|
|
|
return None;
|
2024-03-16 12:24:44 +08:00
|
|
|
};
|
2023-05-15 05:48:17 +08:00
|
|
|
BinaryPrimary {
|
|
|
|
arg_left: self.arg(start).to_owned(),
|
|
|
|
arg_right: self.arg(start + 2).to_owned(),
|
2024-03-16 12:24:44 +08:00
|
|
|
token,
|
2023-05-15 05:48:17 +08:00
|
|
|
range: start..start + 3,
|
|
|
|
}
|
|
|
|
.into_some_box()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_parenthetical(&mut self, start: usize, end: usize) -> Option<Box<dyn Expression>> {
|
|
|
|
// We need at least three arguments: open paren, argument, close paren.
|
|
|
|
if start + 3 >= end {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Must start with an open expression.
|
2024-03-16 12:24:44 +08:00
|
|
|
if token_for_string(self.arg(start)) != Token::ParenOpen {
|
2023-05-15 05:48:17 +08:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse a subexpression.
|
2024-03-09 18:08:16 +08:00
|
|
|
let subexpr = self.parse_expression(start + 1, end)?;
|
2023-05-15 05:48:17 +08:00
|
|
|
|
|
|
|
// 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),
|
|
|
|
);
|
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
if token_for_string(self.arg(close_index)) != Token::ParenClose {
|
2023-05-15 05:48:17 +08:00
|
|
|
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<Box<dyn Expression>> {
|
|
|
|
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<Box<dyn Expression>> {
|
|
|
|
assert!(end - start == 3);
|
2024-03-16 12:24:44 +08:00
|
|
|
|
2023-05-15 05:48:17 +08:00
|
|
|
let center_token = token_for_string(self.arg(start + 1));
|
2024-03-16 12:24:44 +08:00
|
|
|
|
|
|
|
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,
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
.into_some_box()
|
2023-05-15 05:48:17 +08:00
|
|
|
} else {
|
2024-03-16 12:24:44 +08:00
|
|
|
self.parse_unary_expression(start, end)
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_4_arg_expression(
|
|
|
|
&mut self,
|
|
|
|
start: usize,
|
|
|
|
end: usize,
|
|
|
|
) -> Option<Box<dyn Expression>> {
|
|
|
|
assert!(end - start == 4);
|
2024-03-16 12:24:44 +08:00
|
|
|
|
|
|
|
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,
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
.into_some_box()
|
|
|
|
} else if first_token == Token::ParenOpen {
|
|
|
|
self.parse_parenthetical(start, end)
|
2023-05-15 05:48:17 +08:00
|
|
|
} else {
|
2024-03-16 12:24:44 +08:00
|
|
|
self.parse_combining_expression(start, end)
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_expression(&mut self, start: usize, end: usize) -> Option<Box<dyn Expression>> {
|
|
|
|
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<Box<dyn Expression>> {
|
|
|
|
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<f64, Error> {
|
|
|
|
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<WString>) -> bool {
|
|
|
|
let floating = parse_double(arg);
|
|
|
|
let integral: Result<i64, Error> = fish_wcstol(arg);
|
2024-01-27 19:05:47 +08:00
|
|
|
if let Ok(int) = integral {
|
2023-05-15 05:48:17 +08:00
|
|
|
// Here the value is just an integer; ignore the floating point parse because it may be
|
|
|
|
// invalid (e.g. not a representable integer).
|
2024-01-27 19:05:47 +08:00
|
|
|
*number = Number::new(int, 0.0);
|
2023-05-15 05:48:17 +08:00
|
|
|
true
|
2024-05-01 18:28:15 +08:00
|
|
|
} else if floating.is_ok_and(|f| f.is_finite())
|
|
|
|
&& integral.is_err_and(|i| i != Error::Overflow)
|
2023-05-15 05:48:17 +08:00
|
|
|
{
|
|
|
|
// 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));
|
|
|
|
}
|
2024-01-27 19:05:47 +08:00
|
|
|
} else if floating.map_or(false, |x| x.is_nan()) {
|
2023-05-15 05:48:17 +08:00
|
|
|
// NaN is an error as far as we're concerned.
|
|
|
|
errors.push(wgettext!("Not a number").to_owned());
|
2024-01-27 19:05:47 +08:00
|
|
|
} else if floating.map_or(false, |x| x.is_infinite()) {
|
2023-05-15 05:48:17 +08:00
|
|
|
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(
|
2024-03-16 12:24:44 +08:00
|
|
|
token: BinaryToken,
|
2023-05-15 05:48:17 +08:00
|
|
|
left: &wstr,
|
|
|
|
right: &wstr,
|
|
|
|
errors: &mut Vec<WString>,
|
|
|
|
) -> bool {
|
|
|
|
match token {
|
2024-03-16 12:24:44 +08:00
|
|
|
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,
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
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,
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unary_primary_evaluate(
|
2024-03-16 12:24:44 +08:00
|
|
|
token: UnaryToken,
|
2023-05-15 05:48:17 +08:00
|
|
|
arg: &wstr,
|
2023-10-09 05:10:05 +08:00
|
|
|
streams: &mut IoStreams,
|
2023-05-15 05:48:17 +08:00
|
|
|
errors: &mut Vec<WString>,
|
|
|
|
) -> bool {
|
|
|
|
match token {
|
2024-03-16 12:24:44 +08:00
|
|
|
#[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,
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
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
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
2024-03-16 12:24:44 +08:00
|
|
|
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(),
|
|
|
|
},
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/// 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.
|
2023-10-09 05:22:27 +08:00
|
|
|
pub fn test(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option<c_int> {
|
2023-05-15 05:48:17 +08:00
|
|
|
// 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 {
|
2023-08-14 05:39:46 +08:00
|
|
|
streams
|
|
|
|
.err
|
|
|
|
.appendln(wgettext!("[: the last argument must be ']'"));
|
2023-10-09 05:22:27 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, program_name);
|
2023-05-15 05:48:17 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect the arguments into a list.
|
|
|
|
let args: Vec<WString> = argv[1..argc + 1]
|
|
|
|
.iter()
|
|
|
|
.map(|&arg| arg.to_owned())
|
|
|
|
.collect();
|
|
|
|
let args: &[WString] = &args;
|
|
|
|
|
2024-04-21 20:25:54 +08:00
|
|
|
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());
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
return STATUS_INVALID_ARGS; // Per 1003.1, exit false.
|
|
|
|
} else if argc == 1 {
|
2024-04-21 20:25:54 +08:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
2023-05-15 05:48:17 +08:00
|
|
|
// 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);
|
2023-10-09 05:22:27 +08:00
|
|
|
streams.err.append(parser.current_line());
|
2023-05-15 05:48:17 +08:00
|
|
|
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 {
|
2023-10-09 05:22:27 +08:00
|
|
|
streams.err.appendln(&eval_error);
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
// Add a backtrace but not the "see help" message
|
|
|
|
// because this isn't about passing the wrong options.
|
2023-10-09 05:22:27 +08:00
|
|
|
streams.err.append(parser.current_line());
|
2023-05-15 05:48:17 +08:00
|
|
|
}
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if result {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|