use std::collections::HashMap;
use std::fs::{self, Metadata};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use crate::glob::matchpat;
use crate::ported::zsh_h::{
COND_AND, COND_EF, COND_EQ, COND_GE, COND_GT, COND_LE, COND_LT, COND_NE, COND_NOT, COND_NT,
COND_OR, COND_OT, COND_REGEX, COND_STRDEQ, COND_STREQ, COND_STRGTR, COND_STRLT, COND_STRNEQ,
};
use std::os::unix::io::FromRawFd;
use std::io::Write;
pub fn evalcond( args: &[&str],
options: &HashMap<String, bool>,
variables: &HashMap<String, String>,
posix_mode: bool,
) -> i32 {
if args.is_empty() { return 1; }
let toks: Vec<&str> = args
.iter()
.filter(|s| !matches!(**s, "[" | "]" | "[[" | "]]"))
.copied()
.collect();
if toks.is_empty() { return 1; }
fn walk(
toks: &[&str],
pos: &mut usize,
opts: &HashMap<String, bool>,
vars: &HashMap<String, String>,
posix: bool,
prec: u8,
) -> i32 {
let b2i = |b: bool| -> i32 { if b { 0 } else { 1 } };
let peek = |i: usize| -> Option<&str> { toks.get(i).copied() };
match prec {
0 => {
let mut left = walk(toks, pos, opts, vars, posix, 1);
while peek(*pos) == Some("||") || peek(*pos) == Some("-o") {
*pos += 1;
let right = walk(toks, pos, opts, vars, posix, 1);
left = if left == 0 { 0 } else if left >= 2 { left } else { right };
}
left
}
1 => {
let mut left = walk(toks, pos, opts, vars, posix, 2);
while peek(*pos) == Some("&&") || peek(*pos) == Some("-a") {
*pos += 1;
let right = walk(toks, pos, opts, vars, posix, 2);
left = if left != 0 { left } else { right };
}
left
}
2 => {
if peek(*pos) == Some("!") {
*pos += 1;
let r = walk(toks, pos, opts, vars, posix, 2);
if r < 2 { if r == 0 { 1 } else { 0 } } else { r }
} else {
walk(toks, pos, opts, vars, posix, 3)
}
}
_ => {
if peek(*pos) == Some("(") {
*pos += 1;
let r = walk(toks, pos, opts, vars, posix, 0);
if peek(*pos) != Some(")") { return 2; }
*pos += 1;
return r;
}
if let Some(tok) = peek(*pos) {
if tok.starts_with('-') && tok.len() == 2 {
let op = tok.chars().nth(1).unwrap();
if matches!(op,
'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'k'|'L'|'n'|'o'
|'p'|'r'|'s'|'S'|'t'|'u'|'v'|'w'|'x'|'z'|'G'|'N'|'O'
) {
*pos += 1;
let arg = match peek(*pos) {
Some(a) => { *pos += 1; a.to_string() }
None => return 2,
};
return match op {
'a' | 'e' => b2i(Path::new(&arg).exists()), 'b' => b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFBLK as u32),
'c' => b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFCHR as u32),
'd' => b2i(Path::new(&arg).is_dir()),
'f' => b2i(Path::new(&arg).is_file()),
'g' => b2i(dostat(&arg) & libc::S_ISGID as u32 != 0),
'h' | 'L' => b2i(dolstat(&arg) & libc::S_IFMT as u32 == libc::S_IFLNK as u32),
'k' => b2i(dostat(&arg) & libc::S_ISVTX as u32 != 0),
'p' => b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFIFO as u32),
'r' => b2i(doaccess(&arg, 4) != 0), 's' => b2i(getstat(&arg).map(|m| m.len() > 0).unwrap_or(false)),
'S' => b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFSOCK as u32),
'u' => b2i(dostat(&arg) & libc::S_ISUID as u32 != 0),
'w' => b2i(doaccess(&arg, 2) != 0), 'x' => {
if crate::ported::utils::privasserted() { let mode = dostat(&arg);
let s_ixugo = 0o111u32; let is_dir = mode & libc::S_IFMT as u32
== libc::S_IFDIR as u32;
b2i((mode & s_ixugo) != 0 || is_dir) } else { b2i(doaccess(&arg, 1) != 0) }
}
'O' => b2i(getstat(&arg).map(|m| m.uid() == unsafe { libc::geteuid() })
.unwrap_or(false)),
'G' => b2i(getstat(&arg).map(|m| m.gid() == unsafe { libc::getegid() })
.unwrap_or(false)),
'N' => b2i(getstat(&arg).map(|m| m.mtime() >= m.atime()).unwrap_or(false)),
'n' => b2i(!arg.is_empty()),
'z' => b2i(arg.is_empty()),
'o' => {
let r = optison("test", &arg); if r != 3 { r }
else if opts.contains_key(&arg) { b2i(opts[&arg]) }
else { 3 }
}
'v' => b2i(vars.contains_key(&arg)),
't' => crate::ported::math::mathevali(&arg)
.map(|fd| b2i(unsafe { libc::isatty(fd as i32) } != 0))
.unwrap_or(2),
_ => 2,
};
}
}
}
let left = match peek(*pos) {
Some(a) => { *pos += 1; a.to_string() }
None => return 2,
};
let code: Option<i32> = peek(*pos).and_then(|t| match t {
"=" => Some(COND_STREQ),
"==" => Some(COND_STRDEQ),
"!=" => Some(COND_STRNEQ),
"<" => Some(COND_STRLT),
">" => Some(COND_STRGTR),
"-eq" => Some(COND_EQ),
"-ne" => Some(COND_NE),
"-lt" => Some(COND_LT),
"-gt" => Some(COND_GT),
"-le" => Some(COND_LE),
"-ge" => Some(COND_GE),
"-nt" => Some(COND_NT),
"-ot" => Some(COND_OT),
"-ef" => Some(COND_EF),
"=~" | "-regex-match" => Some(COND_REGEX),
_ => None,
});
if let Some(code) = code {
*pos += 1;
let right = match peek(*pos) {
Some(a) => { *pos += 1; a.to_string() }
None => return 2,
};
let parse_num = |s: &str| -> Option<f64> {
let t = s.trim();
if posix {
t.parse::<i64>().ok().map(|i| i as f64)
} else {
crate::ported::math::mathevali(t)
.ok()
.map(|i| i as f64)
.or_else(|| t.parse::<i64>().ok().map(|i| i as f64))
.or_else(|| t.parse::<f64>().ok())
}
};
let num_cmp = |l: &str, r: &str, f: fn(f64, f64) -> bool| -> i32 {
match (parse_num(l), parse_num(r)) {
(Some(a), Some(b)) => b2i(f(a, b)),
_ => 2,
}
};
let mtime_cmp = |l: &str, r: &str, f: fn(i64, i64) -> bool| -> i32 {
let lm = match getstat(l) { Some(m) => m, None => return 1 };
let rm = match getstat(r) { Some(m) => m, None => return 1 };
b2i(f(lm.mtime(), rm.mtime()))
};
let strpat = |pat: &str, text: &str| -> bool {
if posix {
text == pat
} else {
let extended = isset(crate::ported::zsh_h::EXTENDEDGLOB);
let case_sensitive = isset(crate::ported::zsh_h::CASEGLOB);
matchpat(pat, text, extended, case_sensitive)
}
};
return match code {
c if c == COND_STREQ || c == COND_STRDEQ => b2i(strpat(&right, &left)),
c if c == COND_STRNEQ => b2i(!strpat(&right, &left)),
c if c == COND_STRLT => b2i(left.as_str() < right.as_str()),
c if c == COND_STRGTR => b2i(left.as_str() > right.as_str()),
c if c == COND_EQ => num_cmp(&left, &right, |a, b| a == b),
c if c == COND_NE => num_cmp(&left, &right, |a, b| a != b),
c if c == COND_LT => num_cmp(&left, &right, |a, b| a < b),
c if c == COND_GT => num_cmp(&left, &right, |a, b| a > b),
c if c == COND_LE => num_cmp(&left, &right, |a, b| a <= b),
c if c == COND_GE => num_cmp(&left, &right, |a, b| a >= b),
c if c == COND_NT => mtime_cmp(&left, &right, |a, b| a > b),
c if c == COND_OT => mtime_cmp(&left, &right, |a, b| a < b),
c if c == COND_EF => {
let lm = match getstat(&left) { Some(m) => m, None => return 1 };
let rm = match getstat(&right) { Some(m) => m, None => return 1 };
b2i(lm.dev() == rm.dev() && lm.ino() == rm.ino())
}
c if c == COND_REGEX => {
#[cfg(feature = "regex")]
{
match regex::Regex::new(&right) {
Ok(re) => b2i(re.is_match(&left)),
Err(_) => 2,
}
}
#[cfg(not(feature = "regex"))]
{
let extended = isset(crate::ported::zsh_h::EXTENDEDGLOB);
let case_sensitive = isset(crate::ported::zsh_h::CASEGLOB);
b2i(matchpat(&right, &left, extended, case_sensitive))
}
}
_ => 2,
};
}
b2i(!left.is_empty())
}
}
}
let mut pos = 0usize;
let r = walk(&toks, &mut pos, options, variables, posix_mode, 0);
if pos != toks.len() { 2 } else { r }
}
pub fn doaccess(s: &str, c: i32) -> i32 { let cs = match std::ffi::CString::new(crate::ported::utils::unmeta(s)) { Ok(v) => v, Err(_) => return 0,
};
(unsafe { libc::access(cs.as_ptr(), c) } == 0) as i32 }
pub fn getstat(s: &str) -> Option<Metadata> { if let Some(rest) = s.strip_prefix("/dev/fd/") { if let Ok(fd) = rest.parse::<i32>() { let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } != 0 {
return None;
}
let dup_fd = unsafe { libc::dup(fd) };
if dup_fd < 0 {
return None;
}
let f = unsafe { std::fs::File::from_raw_fd(dup_fd) };
return f.metadata().ok();
}
}
let us = crate::ported::utils::unmeta(s);
fs::metadata(&us).ok() }
pub fn dostat(s: &str) -> u32 { getstat(s).map(|m| m.mode()).unwrap_or(0)
}
pub fn dolstat(s: &str) -> u32 { let us = crate::ported::utils::unmeta(s); fs::symlink_metadata(&us).map(|m| m.mode()).unwrap_or(0)
}
pub fn optison(name: &str, s: &str) -> i32 { let i: i32 = if s.len() == 1 { crate::ported::options::optlookupc(s.as_bytes()[0] as char) } else {
crate::ported::options::optlookup(s) };
if i == 0 { if isset(crate::ported::zsh_h::POSIXBUILTINS) { return 1; } else {
crate::ported::utils::zwarnnam(name, &format!("no such option: {}", s)); return 3; }
} else if i < 0 { if unset(-i) { 0 } else { 1 } } else if isset(i) { 0 } else { 1 } }
use crate::ported::zsh_h::{isset, unset};
pub fn cond_str(args: &[String], num: usize, raw: bool) -> String { let s = match args.get(num) { Some(v) => v.clone(),
None => return String::new(),
};
if crate::ported::utils::has_token(&s) { let expanded = crate::ported::subst::singsub(&s); if !raw {
crate::ported::lex::untokenize(&expanded) } else {
expanded
}
} else {
s }
}
pub fn cond_val(args: &[String], num: usize) -> i64 { let raw = match args.get(num) {
Some(v) => v.clone(),
None => return 0,
};
let s = if crate::ported::utils::has_token(&raw) { let expanded = crate::ported::subst::singsub(&raw); crate::ported::lex::untokenize(&expanded) } else {
raw
};
crate::ported::math::mathevali(&s).unwrap_or(0) }
pub fn cond_match(args: &[String], num: usize, str: &str) -> bool { let p_raw = match args.get(num) {
Some(v) => v,
None => return false,
};
let p = crate::ported::subst::singsub(p_raw); let extended = isset(crate::ported::zsh_h::EXTENDEDGLOB);
let case_sensitive = isset(crate::ported::zsh_h::CASEGLOB);
matchpat(&p, str, extended, case_sensitive) }
pub fn tracemodcond(name: &str, args: &[String], inf: bool) { let stderr = std::io::stderr();
let mut out = stderr.lock();
if inf {
let _ = write!(
out,
" {} {} {}",
args.first().map(|s| s.as_str()).unwrap_or(""),
name,
args.get(1).map(|s| s.as_str()).unwrap_or("")
);
} else {
let _ = write!(out, " {}", name);
for a in args {
let _ = write!(out, " {}", a);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
fn empty_maps() -> (HashMap<String, bool>, HashMap<String, String>) {
(HashMap::new(), HashMap::new())
}
#[test]
fn test_string_empty() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["-z", ""], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-z", "hello"], &opts, &vars, true), 1);
assert_eq!(evalcond(&["-n", "hello"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-n", ""], &opts, &vars, true), 1);
}
#[test]
fn test_string_compare() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["hello", "=", "hello"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["hello", "!=", "world"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["abc", "<", "def"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["xyz", ">", "abc"], &opts, &vars, true), 0);
}
#[test]
fn test_numeric_compare() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["5", "-eq", "5"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-ne", "3"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["3", "-lt", "5"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-gt", "3"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-le", "5"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-ge", "5"], &opts, &vars, true), 0);
}
#[test]
fn test_file_exists() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("testfile");
File::create(&file_path).unwrap();
let (opts, vars) = empty_maps();
let path_str = file_path.to_str().unwrap();
assert_eq!(evalcond(&["-e", path_str], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-f", path_str], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-d", path_str], &opts, &vars, true), 1);
}
#[test]
fn test_directory() {
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let path_str = dir.path().to_str().unwrap();
assert_eq!(evalcond(&["-d", path_str], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-f", path_str], &opts, &vars, true), 1);
}
#[test]
fn test_logical_not() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["!", "-z", "hello"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["!", "-n", ""], &opts, &vars, true), 0);
}
#[test]
fn test_logical_and() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["-n", "a", "-a", "-n", "b"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-n", "a", "-a", "-z", "b"], &opts, &vars, true), 1);
}
#[test]
fn test_logical_or() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["-z", "a", "-o", "-n", "b"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-z", "a", "-o", "-z", "b"], &opts, &vars, true), 1);
}
#[test]
fn test_variable_exists() {
let opts = HashMap::new();
let mut vars = HashMap::new();
vars.insert("MYVAR".to_string(), "value".to_string());
assert_eq!(evalcond(&["-v", "MYVAR"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-v", "NOTEXIST"], &opts, &vars, true), 1);
}
#[test]
fn test_minus_s_size_gt_zero() {
use std::io::Write;
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let empty = dir.path().join("empty");
File::create(&empty).unwrap();
assert_eq!(evalcond(&["-s", empty.to_str().unwrap()], &opts, &vars, true), 1,
"c:179 — `-s` must be false for 0-byte file");
let nonempty = dir.path().join("nonempty");
let mut f = File::create(&nonempty).unwrap();
f.write_all(b"data").unwrap();
assert_eq!(evalcond(&["-s", nonempty.to_str().unwrap()], &opts, &vars, true), 0,
"c:179 — `-s` must be true for non-empty file");
let missing = dir.path().join("not_there");
assert_eq!(evalcond(&["-s", missing.to_str().unwrap()], &opts, &vars, true), 1,
"c:179 — `-s` must be false when stat fails (missing file)");
}
#[cfg(unix)]
#[test]
fn test_minus_h_minus_L_detect_symlink_via_lstat() {
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let target = dir.path().join("nonexistent_target");
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let link_s = link.to_str().unwrap();
assert_eq!(evalcond(&["-h", link_s], &opts, &vars, true), 0,
"c:488 — `-h` uses lstat; detects symlink even with missing target");
assert_eq!(evalcond(&["-L", link_s], &opts, &vars, true), 0,
"c:488 — `-L` is same as `-h`");
assert_eq!(evalcond(&["-f", link_s], &opts, &vars, true), 1);
assert_eq!(evalcond(&["-d", link_s], &opts, &vars, true), 1);
}
#[cfg(unix)]
#[test]
fn test_dash_ef_same_inode() {
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let a = dir.path().join("a");
let b = dir.path().join("b");
let c = dir.path().join("c");
File::create(&a).unwrap();
std::fs::hard_link(&a, &b).unwrap();
File::create(&c).unwrap();
let as_ = a.to_str().unwrap();
let bs_ = b.to_str().unwrap();
let cs_ = c.to_str().unwrap();
assert_eq!(evalcond(&[as_, "-ef", bs_], &opts, &vars, true), 0,
"c:179 — hardlinks share st_ino + st_dev → -ef true");
assert_eq!(evalcond(&[as_, "-ef", cs_], &opts, &vars, true), 1,
"c:179 — distinct files → -ef false");
}
#[cfg(unix)]
#[test]
fn test_dash_nt_dash_ot_compare_mtime() {
use std::io::Write;
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let older = dir.path().join("older");
let newer = dir.path().join("newer");
File::create(&older).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1100));
let mut f = File::create(&newer).unwrap();
f.write_all(b"x").unwrap();
let o = older.to_str().unwrap();
let n = newer.to_str().unwrap();
assert_eq!(evalcond(&[n, "-nt", o], &opts, &vars, true), 0,
"c:179 — newer -nt older → true");
assert_eq!(evalcond(&[o, "-nt", n], &opts, &vars, true), 1,
"c:179 — older -nt newer → false");
assert_eq!(evalcond(&[o, "-ot", n], &opts, &vars, true), 0,
"c:179 — older -ot newer → true");
}
#[cfg(unix)]
#[test]
fn test_dash_r_dash_w_access_check() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let file = dir.path().join("rw");
File::create(&file).unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
let p = file.to_str().unwrap();
assert_eq!(evalcond(&["-r", p], &opts, &vars, true), 0,
"c:438 — mode 0600 → readable");
assert_eq!(evalcond(&["-w", p], &opts, &vars, true), 0,
"c:438 — mode 0600 → writable");
if unsafe { libc::geteuid() } != 0 {
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o000)).unwrap();
assert_eq!(evalcond(&["-r", p], &opts, &vars, true), 1,
"c:438 — mode 0000 → not readable (non-root)");
assert_eq!(evalcond(&["-w", p], &opts, &vars, true), 1,
"c:438 — mode 0000 → not writable (non-root)");
}
}
#[test]
fn test_double_negation_cancels() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["!", "!", "-n", "x"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["!", "!", "-z", "x"], &opts, &vars, true), 1);
}
#[test]
fn test_implicit_minus_n_for_bare_arg() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["foo"], &opts, &vars, true), 0,
"non-empty bare arg → true (implicit -n)");
assert_eq!(evalcond(&[""], &opts, &vars, true), 1,
"empty bare arg → false (implicit -n)");
}
#[test]
fn test_cond_str_index_lookup() {
let args = vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()];
assert_eq!(cond_str(&args, 0, false), "alpha");
assert_eq!(cond_str(&args, 2, false), "gamma");
assert_eq!(cond_str(&args, 99, false), "",
"c:525 — out-of-bounds index returns empty (Rust safety)");
}
#[test]
fn test_cond_val_int_coerce() {
let args = vec!["42".to_string(), " -7 ".to_string(), "abc".to_string()];
assert_eq!(cond_val(&args, 0), 42);
assert_eq!(cond_val(&args, 1), -7,
"c:539 — whitespace must trim; negative supported");
assert_eq!(cond_val(&args, 2), 0,
"c:539 — non-numeric returns 0");
assert_eq!(cond_val(&args, 99), 0,
"c:539 — out-of-bounds returns 0");
}
#[test]
fn test_dash_a_dash_e_aliases() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("f");
File::create(&file).unwrap();
let p = file.to_str().unwrap();
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["-e", p], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-a", p], &opts, &vars, true), 0,
"c:179 — -a is alias for -e in zsh test/[[ context");
}
#[test]
fn test_minus_eq_non_numeric_returns_error() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["abc", "-eq", "5"], &opts, &vars, true), 2,
"non-numeric LHS in -eq must return 2 (error)");
}
#[test]
fn test_paren_grouping_and_error_on_missing_close() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["!", "(", "-z", "", ")"], &opts, &vars, true), 1);
assert_eq!(evalcond(&["(", "-z", ""], &opts, &vars, true), 2,
"missing closing `)` must return 2 (cond error)");
}
#[test]
fn getstat_resolves_regular_path() {
assert!(getstat("/").is_some(),
"c:466 — stat('/') must succeed");
assert!(getstat("/nonexistent/path/zzz").is_none(),
"c:464 — nonexistent path returns None");
}
#[cfg(unix)]
#[test]
fn getstat_dev_fd_path_doesnt_dup_bad_fds() {
let _ = getstat("/dev/fd/99"); let _ = getstat("/dev/fd/0");
}
#[test]
fn cond_str_passes_through_when_no_tokens() {
let args = vec!["hello".to_string()];
assert_eq!(cond_str(&args, 0, false), "hello",
"c:534 — token-free string returned as-is");
assert_eq!(cond_str(&args, 0, true), "hello",
"c:534 — token-free string returned as-is regardless of raw");
assert_eq!(cond_str(&args, 99, false), "",
"out-of-bounds num returns empty string");
}
#[test]
fn cond_match_runs_matchpat_through_args_indirection() {
let args = vec!["hello".to_string()];
assert!(cond_match(&args, 0, "hello"),
"literal pattern matches identical text");
assert!(!cond_match(&args, 0, "world"),
"literal pattern rejects non-match");
assert!(!cond_match(&args, 99, "hello"),
"out-of-bounds num returns false");
let args = vec!["*.txt".to_string()];
assert!(cond_match(&args, 0, "file.txt"),
"c:556-557 — pattern `*.txt` matches text `file.txt`");
let args = vec!["file.txt".to_string()];
assert!(!cond_match(&args, 0, "*.txt"),
"c:556-557 — literal pattern `file.txt` does NOT match text `*.txt` \
(this catches the swapped-arg regression)");
}
#[test]
fn cond_val_routes_through_mathevali() {
let args = vec![
"1+2".to_string(),
"10/2".to_string(),
"0x10".to_string(),
"2**8".to_string(),
];
assert_eq!(cond_val(&args, 0), 3,
"c:548 — `mathevali(\"1+2\")` evaluates the expression");
assert_eq!(cond_val(&args, 1), 5,
"c:548 — `mathevali(\"10/2\")` evaluates the expression");
assert_eq!(cond_val(&args, 2), 16,
"c:548 — `mathevali(\"0x10\")` parses hex");
assert_eq!(cond_val(&args, 3), 256,
"c:548 — `mathevali(\"2**8\")` evaluates the expression");
}
#[test]
fn cond_val_plain_integers() {
let args = vec![
"0".to_string(),
"-42".to_string(),
"123456789".to_string(),
];
assert_eq!(cond_val(&args, 0), 0);
assert_eq!(cond_val(&args, 1), -42);
assert_eq!(cond_val(&args, 2), 123456789);
assert_eq!(cond_val(&args, 99), 0,
"out-of-bounds num returns 0");
}
#[test]
fn evalcond_dash_t_accepts_arithmetic_per_cond_c() {
let (opts, vars) = empty_maps();
let result = evalcond(&["-t", "1+0"], &opts, &vars, true);
assert!(result == 0 || result == 1,
"c:330 — `-t 1+0` must mathevali to fd 1 (not parse fail), got {}", result);
let result = evalcond(&["-t", "0"], &opts, &vars, true);
assert!(result == 0 || result == 1,
"c:330 — `-t 0` plain digit still works, got {}", result);
}
#[test]
fn evalcond_int_compare_routes_through_mathevali() {
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["1+2", "-eq", "3"], &opts, &vars, false), 0,
"c:415 — `1+2 -eq 3` must mathevali LHS to 3, return 0");
assert_eq!(evalcond(&["4", "-gt", "1+2"], &opts, &vars, false), 0,
"c:415 — `4 -gt 1+2` must mathevali RHS to 3, return 0");
assert_eq!(evalcond(&["1+2", "-eq", "3"], &opts, &vars, true), 2,
"POSIX — no mathevali; non-numeric LHS = error");
assert_eq!(evalcond(&["5", "-eq", "5"], &opts, &vars, true), 0,
"POSIX — plain integers compare normally");
}
}