use crate::ported::options::opt_state_get;
use crate::ported::sort::zstrcmp;
use crate::ported::builtin::LASTVAL;
use crate::ported::pattern::haswilds;
use crate::ported::signals::unqueue_signals;
use crate::ported::string::dyncat;
use crate::ported::utils::{zerr, errflag, init_dirsav, lchdir, restoredir};
use crate::ported::zsh_h::{isset, redir, Bnull, Bnullkeep, Dnull, Inang, Nularg, Outang, Pound, Snull, BAREGLOBQUAL, BRACECCL, CASEGLOB, ERRFLAG_INT, EXTENDEDGLOB, GLOBDOTS, GLOBSTARSHORT, IS_DASH, LISTTYPES, MARKDIRS, Meta, MULTIOS, NULLGLOB, NUMERICGLOBSORT, PP_UNKWN, PREFORK_SINGLE, REDIR_CLOSE, REDIR_ERRWRITE, REDIR_MERGEIN, REDIR_MERGEOUT, SHGLOB, SUB_ALL, SUB_END, SUB_GLOBAL, SUB_LIST, SUB_LONG, SUB_MATCH, SUB_REST, SUB_START, SUB_SUBSTR, ZSHTOK_SHGLOB, ZSHTOK_SUBST};
use std::sync::atomic::Ordering;
use std::collections::HashSet;
use std::fs::{self, Metadata};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Component, Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::ported::subst::LinkList;
use crate::subst::prefork;
#[derive(Debug, Clone)]
pub struct gmatch {
pub name: String,
pub path: PathBuf,
pub size: u64,
pub atime: i64,
pub mtime: i64,
pub ctime: i64,
pub links: u64,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub dev: u64,
pub ino: u64,
pub target_size: u64,
pub target_atime: i64,
pub target_mtime: i64,
pub target_ctime: i64,
pub target_links: u64,
pub sort_strings: Vec<String>,
}
impl GlobOptSnapshot {
pub fn capture() -> Self {
Self {
bareglobqual: isset(BAREGLOBQUAL),
braceccl: isset(BRACECCL),
caseglob: isset(CASEGLOB),
extendedglob: isset(EXTENDEDGLOB),
globdots: isset(GLOBDOTS),
globstarshort: isset(GLOBSTARSHORT),
listtypes: isset(LISTTYPES),
markdirs: isset(MARKDIRS),
nullglob: isset(NULLGLOB),
numericglobsort: isset(NUMERICGLOBSORT),
}
}
}
thread_local! {
static GLOB_OPTS_TLS: std::cell::RefCell<Option<GlobOptSnapshot>> =
const { std::cell::RefCell::new(None) };
}
pub const GS_NAME: i32 = 1;
impl Drop for GlobOptsGuard {
fn drop(&mut self) {
if self.populated {
GLOB_OPTS_TLS.with_borrow_mut(|g| *g = None);
}
}
}
pub const GS_DEPTH: i32 = 2; pub const GS_EXEC: i32 = 4;
pub const GS_SHIFT_BASE: i32 = 8;
pub const GS_SIZE: i32 = GS_SHIFT_BASE; pub const GS_ATIME: i32 = GS_SHIFT_BASE << 1; pub const GS_MTIME: i32 = GS_SHIFT_BASE << 2; pub const GS_CTIME: i32 = GS_SHIFT_BASE << 3; pub const GS_LINKS: i32 = GS_SHIFT_BASE << 4;
pub const GS_SHIFT: i32 = 5;
pub const GS__SIZE: i32 = GS_SIZE << GS_SHIFT; pub const GS__ATIME: i32 = GS_ATIME << GS_SHIFT; pub const GS__MTIME: i32 = GS_MTIME << GS_SHIFT; pub const GS__CTIME: i32 = GS_CTIME << GS_SHIFT; pub const GS__LINKS: i32 = GS_LINKS << GS_SHIFT;
pub const GS_DESC: i32 = GS_SHIFT_BASE << (2 * GS_SHIFT); pub const GS_NONE: i32 = GS_SHIFT_BASE << (2 * GS_SHIFT + 1);
pub const GS_NORMAL: i32 = GS_SIZE | GS_ATIME | GS_MTIME | GS_CTIME | GS_LINKS; pub const GS_LINKED: i32 = GS_NORMAL << GS_SHIFT;
pub const TT_DAYS: i32 = 0; pub const TT_HOURS: i32 = 1; pub const TT_MINS: i32 = 2; pub const TT_WEEKS: i32 = 3; pub const TT_MONTHS: i32 = 4; pub const TT_SECONDS: i32 = 5;
pub const TT_BYTES: i32 = 0; pub const TT_POSIX_BLOCKS: i32 = 1; pub const TT_KILOBYTES: i32 = 2; pub const TT_MEGABYTES: i32 = 3; pub const TT_GIGABYTES: i32 = 4; pub const TT_TERABYTES: i32 = 5;
pub const MAX_SORTS: usize = 12;
#[allow(non_camel_case_types)]
pub struct globdata {
pub matches: Vec<gmatch>,
pub qualifiers: Option<qualifier_set>,
pub pathbuf: String, pub pathpos: usize, pub matchct: i32, pub pathbufsz: usize, pub pathbufcwd: i32, pub gf_nullglob: i32, pub gf_markdirs: i32, pub gf_noglobdots: i32, pub gf_listtypes: i32, pub gf_pre_words: Option<Vec<String>>, pub gf_post_words: Option<Vec<String>>, }
pub static CURGLOBDATA: std::sync::Mutex<globdata> = std::sync::Mutex::new(globdata {
matches: Vec::new(),
qualifiers: None,
pathbuf: String::new(),
pathpos: 0,
matchct: 0,
pathbufsz: 0,
pathbufcwd: 0,
gf_nullglob: 0,
gf_markdirs: 0,
gf_noglobdots: 0,
gf_listtypes: 0,
gf_pre_words: None,
gf_post_words: None,
});
pub static BADCSHGLOB: std::sync::atomic::AtomicI32 =
std::sync::atomic::AtomicI32::new(0);
#[allow(non_camel_case_types)]
pub struct complist {
pub next: Option<Box<complist>>, pub pat: crate::ported::pattern::Patprog, pub closure: i32, pub follow: i32, }
pub fn addpath(s: &mut String, l: &str) {
s.push_str(l);
if !s.ends_with('/') {
s.push('/');
}
}
pub fn statfullpath(s: &str, st: &str, l: bool) -> Option<Metadata> {
let full = if st.is_empty() {
if s.is_empty() {
".".to_string()
} else {
s.to_string()
}
} else {
format!("{}{}", s, st)
};
if l {
fs::metadata(&full).ok()
} else {
fs::symlink_metadata(&full).ok()
}
}
#[allow(unused_variables)]
pub fn insert(s: &str, checked: i32) {
crate::ported::signals::queue_signals();
let mut inserts_local: Option<Vec<String>> = None;
let mut news: String = s.to_string(); let mut statted: i32 = 0;
let (gf_listtypes, gf_markdirs, gf_follow): (i32, i32, i32) = {
(0, 0, 0)
};
let mut buf: Option<Metadata> = None; let mut buf2: Option<Metadata> = None;
let mut checked_v = checked;
if gf_listtypes != 0 || gf_markdirs != 0 {
let st = statfullpath(s, "", true); match st {
None => {
unqueue_signals(); return; }
Some(m) => {
buf = Some(m.clone()); checked_v = 1; statted = 1; }
}
use std::os::unix::fs::MetadataExt;
let mut mode = buf.as_ref().map(|b| b.mode()).unwrap_or(0); if gf_follow != 0 {
let is_lnk = buf
.as_ref()
.map(|b| (b.mode() & 0o170000) == 0o120000)
.unwrap_or(false);
if !is_lnk {
buf2 = buf.clone(); } else {
buf2 = statfullpath(s, "", false); if buf2.is_none() {
buf2 = buf.clone();
} }
statted |= 2; mode = buf2.as_ref().map(|b| b.mode()).unwrap_or(mode); }
let is_dir = (mode & 0o170000) == 0o040000;
if gf_listtypes != 0 || is_dir {
let marker = file_type(mode); news.push(marker); }
}
let qualct: i32 = 0; let qualorct: i32 = 0; let pathbuf_local: String = String::new();
if qualct != 0 || qualorct != 0 {
if statted == 0 {
if statfullpath(s, "", true).is_none() {
unqueue_signals(); return; }
}
news = dyncat(&pathbuf_local, &news); statted = 1; } else if checked_v == 0 {
if statfullpath(s, "", true).is_none() {
unqueue_signals(); return; }
news = dyncat(&pathbuf_local, &news); } else {
news = dyncat(&pathbuf_local, &news); }
let mut inserts_idx: usize = 0;
loop {
let cur_news: String = if let Some(list) = inserts_local.as_ref() {
if inserts_idx >= list.len() {
break;
}
let s = crate::ported::mem::dupstring(&list[inserts_idx]);
inserts_idx += 1;
s
} else {
news.clone()
};
let mut cur_news = cur_news;
let colonmod: Option<String> = None; if let Some(_mod_str) = colonmod { }
let gf_sorts: i32 = 0; if statted == 0 && (gf_sorts & GS_NORMAL) != 0 {
buf = statfullpath(s, "", true); statted = 1; }
if (statted & 2) == 0 && (gf_sorts & GS_LINKED) != 0 {
if statted != 0 {
use std::os::unix::fs::MetadataExt;
let is_lnk = buf
.as_ref()
.map(|b| (b.mode() & 0o170000) == 0o120000)
.unwrap_or(false);
if !is_lnk {
buf2 = buf.clone(); } else {
buf2 = statfullpath(s, "", false); if buf2.is_none() {
buf2 = buf.clone();
} }
} else {
buf2 = statfullpath(s, "", false); if buf2.is_none() {
buf2 = statfullpath(s, "", true); }
}
statted |= 2; }
let mut entry = gmatch {
name: cur_news.clone(),
path: PathBuf::from(&cur_news),
size: 0,
atime: 0,
mtime: 0,
ctime: 0,
links: 0,
mode: 0,
uid: 0,
gid: 0,
dev: 0,
ino: 0,
target_size: 0,
target_atime: 0,
target_mtime: 0,
target_ctime: 0,
target_links: 0,
sort_strings: Vec::new(),
};
if (statted & 1) != 0 {
use std::os::unix::fs::MetadataExt;
if let Some(b) = buf.as_ref() {
entry.size = b.size(); entry.atime = b.atime(); entry.mtime = b.mtime(); entry.ctime = b.ctime(); entry.links = b.nlink(); }
}
if (statted & 2) != 0 {
use std::os::unix::fs::MetadataExt;
if let Some(b) = buf2.as_ref() {
entry.target_size = b.size(); entry.target_atime = b.atime(); entry.target_mtime = b.mtime(); entry.target_ctime = b.ctime(); entry.target_links = b.nlink(); }
}
let _ = entry;
if inserts_local.is_none() {
break; }
let _ = &mut cur_news;
}
let _ = (inserts_idx, buf, buf2); unqueue_signals(); }
fn scanner(state: &mut globdata, components: &[PatternComponent], depth: usize) {
if components.is_empty() {
return;
}
let base_path = if state.pathbuf.is_empty() {
".".to_string()
} else {
state.pathbuf.clone()
};
match &components[0] {
PatternComponent::Pattern(pat) => {
scan_pattern(state, &base_path, pat, &components[1..], depth);
}
PatternComponent::Recursive { follow_links } => {
scanner(state, &components[1..], depth);
scan_recursive(state, &base_path, &components[1..], *follow_links, depth);
}
}
}
pub fn parsecomplist(instr: &str) -> Option<Box<complist>> {
let p1: crate::ported::pattern::Patprog; let l1: Box<complist>; let gf_noglobdots: i32 = CURGLOBDATA
.lock()
.unwrap_or_else(|e| e.into_inner())
.gf_noglobdots; let compflags: i32 = if gf_noglobdots != 0 {
crate::ported::zsh_h::PAT_FILE | crate::ported::zsh_h::PAT_NOGLD
} else {
crate::ported::zsh_h::PAT_FILE
};
let chars: Vec<char> = instr.chars().collect();
if chars.len() >= 2
&& chars[0] == crate::ported::zsh_h::Star
&& chars[1] == crate::ported::zsh_h::Star
{
let mut shortglob: i32 = 0; let cond_a = chars.get(2) == Some(&'/'); let cond_b = chars.get(2) == Some(&crate::ported::zsh_h::Star)
&& chars.get(3) == Some(&'/'); let cond_c = {
shortglob = if crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBSTARSHORT) {
1
} else {
0
};
shortglob != 0
}; if cond_a || cond_b || cond_c {
let follow: i32 = if chars.get(2) == Some(&crate::ported::zsh_h::Star) {
1
} else {
0
}; let advance: usize = (if shortglob != 0 { 1 } else { 3 }) + follow as usize;
let next_instr: String = chars[advance..].iter().collect();
let next_l = parsecomplist(&next_instr); if next_l.is_none() {
crate::ported::utils::errflag.fetch_or(
crate::ported::zsh_h::ERRFLAG_ERROR,
std::sync::atomic::Ordering::Relaxed,
); return None; }
let pat = crate::ported::pattern::patcompile(
"",
compflags | crate::ported::zsh_h::PAT_ANY,
None,
)?; l1 = Box::new(complist {
next: next_l,
pat,
closure: 1, follow, });
return Some(l1); }
}
let zpc = crate::ported::pattern::zpc_special
.lock()
.unwrap_or_else(|e| e.into_inner());
let inpar_c = zpc[crate::ported::zsh_h::ZPC_INPAR as usize] as char;
let hash_c = zpc[crate::ported::zsh_h::ZPC_HASH as usize] as char;
drop(zpc);
let mut str_after_parens: Option<usize> = None; let mut skip_level: i32 = -1; if chars.first() == Some(&inpar_c) { let mut level: i32 = 1; let mut i: usize = 1; while i < chars.len() && level != 0 { if chars[i] == crate::ported::zsh_h::Inpar {
level += 1; } else if chars[i] == crate::ported::zsh_h::Outpar {
level -= 1; }
i += 1;
}
skip_level = level; str_after_parens = Some(i); }
let parens_balanced = chars.first() == Some(&inpar_c) && skip_level == 0; let after_paren_idx = str_after_parens.unwrap_or(0);
let str_at_hash = parens_balanced && chars.get(after_paren_idx) == Some(&hash_c); let preceded_by_slash =
parens_balanced && after_paren_idx >= 2 && chars.get(after_paren_idx - 2) == Some(&'/');
if parens_balanced && str_at_hash && preceded_by_slash {
let mut cursor: String = chars[1..].iter().collect();
let mut endexp = String::new();
let p1_opt =
crate::ported::pattern::patcompile(&cursor, compflags, Some(&mut endexp)); if p1_opt.is_none() {
return None; }
let p1_real = p1_opt.unwrap();
cursor = endexp; let c2: Vec<char> = cursor.chars().collect();
if c2.first() == Some(&'/')
&& c2.get(1) == Some(&crate::ported::zsh_h::Outpar)
&& c2.get(2) == Some(&crate::ported::zsh_h::Pound)
{
let mut pdflag: i32 = 0; let mut adv = 3; if c2.get(3) == Some(&crate::ported::zsh_h::Pound) {
pdflag = 1; adv = 4; }
let next_instr: String = c2[adv..].iter().collect();
let pat_nonempty = !p1_real.1.is_empty()
&& p1_real.1.get(p1_real.0.startoff as usize).copied().unwrap_or(0) != 0;
let closure = if pat_nonempty { 1 + pdflag } else { 0 };
let next_l = parsecomplist(&next_instr); l1 = Box::new(complist {
next: next_l,
pat: p1_real,
closure,
follow: 0, });
return Some(l1);
}
} else {
let mut endexp = String::new();
let p1_opt = crate::ported::pattern::patcompile(
instr,
compflags | crate::ported::zsh_h::PAT_FILET,
Some(&mut endexp),
); if p1_opt.is_none() {
return None; }
p1 = p1_opt.unwrap();
let cursor: Vec<char> = endexp.chars().collect();
let head = cursor.first().copied();
if head == Some('/') || head.is_none() {
let ef: i32 = if head == Some('/') { 1 } else { 0 }; let next_l: Option<Box<complist>> = if ef != 0 {
let rest: String = cursor[1..].iter().collect();
parsecomplist(&rest) } else {
None
};
if ef != 0 && next_l.is_none() {
return None;
}
l1 = Box::new(complist {
next: next_l,
pat: p1,
closure: 0, follow: 0,
});
return Some(l1);
}
}
crate::ported::utils::errflag.fetch_or(
crate::ported::zsh_h::ERRFLAG_ERROR,
std::sync::atomic::Ordering::Relaxed,
); None }
pub fn parsepat(s: &str) -> Option<Box<complist>> {
crate::ported::pattern::patcompstart(); let chars: Vec<char> = s.chars().collect();
let zpc = crate::ported::pattern::zpc_special
.lock()
.unwrap_or_else(|e| e.into_inner());
let inpar_c = zpc[crate::ported::zsh_h::ZPC_INPAR as usize] as char;
let hash_c = zpc[crate::ported::zsh_h::ZPC_HASH as usize] as char;
let ksh_at_c = zpc[crate::ported::zsh_h::ZPC_KSH_AT as usize] as char;
drop(zpc);
let mut cursor: String = s.to_string();
let first_is_inpar_hash =
chars.first() == Some(&inpar_c) && chars.get(1) == Some(&hash_c); let first_is_ksh_at_inpar_hash = chars.first() == Some(&ksh_at_c)
&& chars.get(1) == Some(&crate::ported::zsh_h::Inpar)
&& chars.get(2) == Some(&hash_c); if first_is_inpar_hash || first_is_ksh_at_inpar_hash {
let skip = if chars.first() == Some(&crate::ported::zsh_h::Inpar) {
2
} else {
3
}; cursor = chars[skip..].iter().collect();
let flag_result = crate::ported::pattern::patgetglobflags(&cursor); if flag_result.is_none() {
return None; }
let (_bits, _assertp, consumed) = flag_result.unwrap();
cursor = cursor[consumed..].to_string();
}
{
let mut gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
if gd.pathbuf.capacity() == 0 {
gd.pathbufsz = libc::PATH_MAX as usize + 1;
gd.pathbuf = String::with_capacity(gd.pathbufsz);
}
debug_assert!(gd.pathbufcwd == 0, "BUG: glob changed directory");
if cursor.starts_with('/') {
cursor = cursor[1..].to_string(); gd.pathbuf.clear();
gd.pathbuf.push('/'); gd.pathpos = 1; } else {
gd.pathbuf.clear(); gd.pathpos = 0;
}
}
parsecomplist(&cursor) }
pub fn qgetnum(s: &str) -> Option<(i64, &str)> {
let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
if end == 0 {
return None;
}
let num = s[..end].parse::<i64>().ok()?;
Some((num, &s[end..]))
}
impl globdata {
pub fn new() -> Self {
globdata {
matches: Vec::new(),
qualifiers: None,
pathbuf: String::with_capacity(4096),
pathpos: 0,
matchct: 0,
pathbufsz: 4096,
pathbufcwd: 0,
gf_nullglob: 0,
gf_markdirs: 0,
gf_noglobdots: 0,
gf_listtypes: 0,
gf_pre_words: None,
gf_post_words: None,
}
}
}
pub fn qgetmodespec(s: &str) -> Option<(u32, char, u32, &str)> {
let mut chars = s.chars().peekable();
let mut spec_who: u32 = 0;
let mut spec_op: char = '\0';
let mut spec_perm: u32 = 0;
if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
let mut mode_str = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() && c < '8' {
mode_str.push(c);
chars.next();
} else {
break;
}
}
if let Ok(mode) = u32::from_str_radix(&mode_str, 8) {
spec_perm = mode;
spec_op = '=';
spec_who = 0o7777;
let rest_pos = s.len() - chars.collect::<String>().len();
return Some((spec_who, spec_op, spec_perm, &s[rest_pos..]));
}
return None;
}
let mut who = 0u32;
while let Some(&c) = chars.peek() {
match c {
'u' => {
who |= 0o4700;
chars.next();
}
'g' => {
who |= 0o2070;
chars.next();
}
'o' => {
who |= 0o1007;
chars.next();
}
'a' => {
who |= 0o7777;
chars.next();
}
_ => break,
}
}
if who == 0 {
who = 0o7777; }
spec_who = who;
spec_op = match chars.next() {
Some('+') => '+',
Some('-') => '-',
Some('=') => '=',
_ => return None,
};
let mut perm = 0u32;
while let Some(&c) = chars.peek() {
match c {
'r' => {
perm |= 0o444;
chars.next();
}
'w' => {
perm |= 0o222;
chars.next();
}
'x' => {
perm |= 0o111;
chars.next();
}
'X' => {
perm |= 0o111;
chars.next();
} 's' => {
perm |= 0o6000;
chars.next();
}
't' => {
perm |= 0o1000;
chars.next();
}
_ => break,
}
}
spec_perm = perm & who;
let rest_pos = s.len() - chars.collect::<String>().len();
Some((spec_who, spec_op, spec_perm, &s[rest_pos..]))
}
pub fn gmatchcmp(
a: &gmatch,
b: &gmatch,
specs: &[i32],
numeric_sort: bool,
) -> std::cmp::Ordering {
for &tp in specs {
let key = tp & !GS_DESC; let follow = (key & GS_LINKED) != 0;
let key_unshifted = if follow { key >> GS_SHIFT } else { key };
let cmp = if key_unshifted == GS_NAME {
zstrcmp(
&a.name,
&b.name,
if numeric_sort {
crate::zsh_h::SORTIT_NUMERICALLY as u32
} else {
0
},
)
} else if key_unshifted == GS_DEPTH {
a.path
.components()
.count()
.cmp(&b.path.components().count())
} else if key_unshifted == GS_SIZE {
if follow {
a.target_size.cmp(&b.target_size)
} else {
a.size.cmp(&b.size)
}
} else if key_unshifted == GS_ATIME {
if follow {
b.target_atime.cmp(&a.target_atime)
} else {
b.atime.cmp(&a.atime)
}
} else if key_unshifted == GS_MTIME {
if follow {
b.target_mtime.cmp(&a.target_mtime)
} else {
b.mtime.cmp(&a.mtime)
}
} else if key_unshifted == GS_CTIME {
if follow {
b.target_ctime.cmp(&a.target_ctime)
} else {
b.ctime.cmp(&a.ctime)
}
} else if key_unshifted == GS_LINKS {
if follow {
b.target_links.cmp(&a.target_links)
} else {
b.links.cmp(&a.links)
}
} else if key_unshifted == GS_EXEC {
let idx = ((key as u32) >> 16) as usize;
let asx = a.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
let bsx = b.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
zstrcmp(
asx,
bsx,
if numeric_sort {
crate::zsh_h::SORTIT_NUMERICALLY as u32
} else {
0
},
)
} else {
std::cmp::Ordering::Equal };
if cmp != std::cmp::Ordering::Equal {
return if (tp & GS_DESC) != 0 {
cmp.reverse()
} else {
cmp
};
}
}
std::cmp::Ordering::Equal
}
pub fn glob_exec_string(s: &str, plus_form: bool) -> Option<(String, usize)> {
if plus_form {
let tt = crate::ported::utils::itype_end(s, false);
if tt == 0 {
zerr("missing identifier after `+'"); return None; }
Some((s[..tt].to_string(), tt))
} else {
match crate::ported::subst::get_strarg(s) {
Some((_del, content, rest)) => {
if rest.is_empty() && content.is_empty() {
zerr("missing end of string"); return None; }
let advance = s.len() - rest.len();
Some((content, advance))
}
None => {
zerr("missing end of string"); None
}
}
}
}
pub fn insert_glob_match(list: &mut Vec<String>, next: usize, data: &str) {
let (pre, post) = {
let gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
(gd.gf_pre_words.clone(), gd.gf_post_words.clone()) };
let mut cur = next;
let n = list.len();
let mut clamp = |i: usize| -> usize { if i > n { n } else { i } };
if let Some(pre_words) = pre { for w in pre_words.iter() { let pos = clamp(cur + 1);
list.insert(pos, w.clone()); cur = pos; }
}
let pos = clamp(cur + 1);
list.insert(pos, data.to_string()); cur = pos;
if let Some(post_words) = post { for w in post_words.iter() { let pos = clamp(cur + 1);
list.insert(pos, w.clone()); cur = pos;
}
}
}
pub fn checkglobqual(
str: &str,
sl: i32,
_nobareglob: i32, sp: &mut Option<usize>,
) -> i32 {
let bytes = str.as_bytes();
let sl = sl as usize;
if sl == 0 || bytes[sl - 1] != b')' {
return 0;
}
let mut paren = 1i32;
let mut i = sl - 1;
while i > 0 {
i -= 1;
match bytes[i] {
b')' => paren += 1,
b'(' => {
paren -= 1;
if paren == 0 {
*sp = Some(i);
return 1; }
}
_ => {}
}
}
0 }
pub fn zglob(list: &mut Vec<String>, np: usize, nountok: i32) {
if np >= list.len() {
return;
}
let node: usize = np; let ostr = list[np].clone();
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBOPT)
|| !haswilds(&ostr)
|| !crate::ported::zsh_h::isset(crate::ported::zsh_h::EXECOPT)
{
if nountok == 0 { list[np] = crate::ported::lex::untokenize(&ostr);
}
return; }
list.remove(np);
let matches = glob_path(&ostr);
if !matches.is_empty() {
BADCSHGLOB.fetch_or(2, std::sync::atomic::Ordering::Relaxed);
} else if crate::ported::zsh_h::isset(crate::ported::zsh_h::CSHNULLGLOB) {
BADCSHGLOB.fetch_or(1, std::sync::atomic::Ordering::Relaxed);
}
if matches.is_empty() {
let nullglob = isset(crate::ported::zsh_h::NULLGLOB); let csh_nullglob = isset(crate::ported::zsh_h::CSHNULLGLOB); if !nullglob && !csh_nullglob && isset(crate::ported::zsh_h::NOMATCH) {
crate::ported::utils::zerr(&format!(
"no matches found: {}",
ostr
));
return; }
let restored = if nountok == 0 {
crate::ported::lex::untokenize(&ostr) } else {
ostr.clone()
};
let pos = if node > list.len() { list.len() } else { node };
list.insert(pos, restored); return;
}
let mut cur = if node == 0 { 0 } else { node - 1 }; for m in matches.iter() {
insert_glob_match(list, cur, m); cur += 1;
if let Some(g) = CURGLOBDATA.lock().ok() {
if let Some(p) = &g.gf_pre_words {
cur += p.len();
}
if let Some(p) = &g.gf_post_words {
cur += p.len();
}
}
}
}
pub fn file_type(filemode: u32) -> char {
let fmt = filemode & libc::S_IFMT as u32;
if fmt == libc::S_IFBLK as u32 {
'#'
} else if fmt == libc::S_IFCHR as u32 {
'%'
} else if fmt == libc::S_IFDIR as u32 {
'/'
} else if fmt == libc::S_IFIFO as u32 {
'|'
} else if fmt == libc::S_IFLNK as u32 {
'@'
} else if fmt == libc::S_IFREG as u32 {
if filemode & 0o111 != 0 {
'*'
} else {
' '
}
} else if fmt == libc::S_IFSOCK as u32 {
'='
} else {
'?'
}
}
pub fn hasbraces(s: &str, brace_ccl: bool) -> bool {
let mut depth = 0;
let mut has_comma = false;
let mut has_dotdot = false;
let mut brace_open: Option<usize> = None;
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if (chars[i] == '\\' || chars[i] == '\u{9f}') && i + 1 < len {
i += 2; continue;
}
match chars[i] {
'\u{8f}' => {
if depth == 0 {
brace_open = Some(i);
}
depth += 1;
}
'\u{90}' if depth > 0 => {
depth -= 1;
if depth == 0 {
if has_comma || has_dotdot {
return true;
}
if brace_ccl {
if let Some(open) = brace_open {
if i > open + 1 {
return true;
}
}
}
has_comma = false;
has_dotdot = false;
brace_open = None;
}
}
'\u{9a}' if depth == 1 => has_comma = true,
'.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' => has_dotdot = true,
_ => {}
}
i += 1;
}
false
}
pub fn bracechardots(s: &str) -> Option<(char, char, i32)> {
let chars: Vec<char> = s.chars().collect();
if chars.len() < 4 {
return None;
}
let dotdot_pos = s.find("..")?;
if dotdot_pos == 0 {
return None;
}
let left = &s[..dotdot_pos];
let right = &s[dotdot_pos + 2..];
let (end_str, incr) = if let Some(pos) = right.find("..") {
let end = &right[..pos];
let inc: i32 = right[pos + 2..].parse().unwrap_or(1);
(end, inc)
} else {
(right, 1)
};
if left.chars().count() == 1 && end_str.chars().count() == 1 {
let c1 = left.chars().next()?;
let c2 = end_str.chars().next()?;
return Some((c1, c2, incr));
}
None
}
pub fn xpandbraces(s: &str, brace_ccl: bool) -> Vec<String> {
if !hasbraces(s, brace_ccl) {
return vec![s.to_string()];
}
let try_expand_one = |s: &str| -> Option<Vec<String>> {
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let start = chars.iter().position(|&c| c == '\u{8f}')?;
let mut depth = 1;
let mut comma_positions = Vec::new();
let mut dotdot_pos = None;
for i in (start + 1)..len {
match chars[i] {
'\u{8f}' => depth += 1,
'\u{90}' => {
depth -= 1;
if depth == 0 {
let prefix: String = chars[..start].iter().collect();
let suffix: String = chars[i + 1..].iter().collect();
let content: String = chars[start + 1..i].iter().collect();
if let Some(dp) = dotdot_pos {
if comma_positions.is_empty() {
if let Some(parts) = expand_range(&prefix, &content, dp, &suffix) {
return Some(parts);
}
let left = &content[..dp];
let right = &content[dp + 2..];
let strip_end = right
.find("..")
.map(|p| &right[..p])
.unwrap_or(right);
let has_digit = left
.chars()
.any(|c| c.is_ascii_digit())
|| strip_end
.chars()
.any(|c| c.is_ascii_digit());
if has_digit {
return Some(vec![format!(
"{}{}{}",
prefix, content, suffix
)]);
}
return None;
}
}
if !comma_positions.is_empty() {
return expand_comma(&prefix, &content, &comma_positions, &suffix);
}
if brace_ccl && !content.is_empty() {
return expand_ccl(&prefix, &content, &suffix);
}
return None;
}
}
'\u{9a}' if depth == 1 => comma_positions.push(i - start - 1),
'.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' && dotdot_pos.is_none() => {
dotdot_pos = Some(i - start - 1);
}
_ => {}
}
}
None
};
let mut results = vec![s.to_string()];
let mut changed = true;
while changed {
changed = false;
let mut new_results = Vec::new();
for item in &results {
if let Some(expanded) = try_expand_one(item) {
new_results.extend(expanded);
changed = true;
} else {
new_results.push(item.clone());
}
}
results = new_results;
}
results
}
pub fn matchpat(pattern_in: &str, text_in: &str, extended: bool, case_sensitive: bool) -> bool {
let a = text_in;
let b = pattern_in;
crate::ported::signals_h::queue_signals(); let (a_eff, b_eff) = if case_sensitive {
(a.to_string(), b.to_string())
} else {
(a.to_lowercase(), b.to_lowercase())
};
let prev_extended = crate::ported::options::opt_state_get("extendedglob");
let prev_caseglob = crate::ported::options::opt_state_get("caseglob");
crate::ported::options::opt_state_set("extendedglob", extended);
crate::ported::options::opt_state_set("caseglob", case_sensitive);
let p_opt = crate::ported::pattern::patcompile(&b_eff, crate::ported::zsh_h::PAT_HEAPDUP, None);
if let Some(v) = prev_extended {
crate::ported::options::opt_state_set("extendedglob", v);
}
if let Some(v) = prev_caseglob {
crate::ported::options::opt_state_set("caseglob", v);
}
let ret = match p_opt {
Some(p) => crate::ported::pattern::pattry(&p, &a_eff), None => {
crate::ported::utils::zerr(&format!("bad pattern: {}", b)); false }
};
crate::ported::signals_h::unqueue_signals(); ret }
pub fn get_match_ret(imd: &imatchdata, b: usize, e: usize) -> String {
if b >= e || b >= imd.str.len() {
return String::new();
}
let e = e.min(imd.str.len());
imd.str[b..e].to_string()
}
pub fn compgetmatch(pat: &str) -> Option<(String, i32)> {
let mut flags: i32 = 0;
let mut pattern = pat.to_string();
if pattern.starts_with('#') {
flags |= SUB_START;
pattern = pattern[1..].to_string();
}
if pattern.starts_with("##") {
flags |= SUB_START | SUB_LONG;
pattern = pattern[2..].to_string();
}
if pattern.ends_with('%') {
flags |= SUB_END;
pattern.pop();
}
if pattern.ends_with("%%") {
flags |= SUB_END | SUB_LONG;
pattern.truncate(pattern.len().saturating_sub(2));
}
Some((pattern, flags))
}
pub fn getmatch(sp: &str, pat: &str, fl: i32, n: i32, replstr: Option<&str>) -> String {
let (prep_pat, prep_fl) = match compgetmatch(pat) {
Some(t) => t,
None => return sp.to_string(), };
let mut buf = sp.to_string();
igetmatch(&mut buf, &prep_pat, prep_fl | fl, n, replstr); buf
}
pub fn getmatcharr(
ap: &[String],
pat: &str,
fl: i32,
n: i32,
replstr: Option<&str>,
) -> Vec<String> {
ap.iter()
.map(|s| getmatch(s, pat, fl, n, replstr))
.collect()
}
pub fn getmatchlist(sp: &mut String, p: &str) -> i32 {
igetmatch(
sp,
p,
SUB_LONG | SUB_GLOBAL | SUB_SUBSTR | SUB_LIST, 0,
None,
) }
pub static IN_EXPANDREDIR: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub fn xpandredir(
fn_: &mut redir, redirtab: &mut Vec<redir>,
) -> i32 {
use std::sync::atomic::Ordering::SeqCst;
let mut ret = 0; let name = match fn_.name.as_deref() {
Some(n) => n.to_string(),
None => return 0,
};
let mut fake: LinkList = LinkList::new();
fake.push_back(name); let mut rf = 0i32;
prefork(
&mut fake, if isset(MULTIOS) { 0 } else { PREFORK_SINGLE },
&mut rf,
);
if errflag.load(SeqCst) == 0
&& isset(MULTIOS)
{
IN_EXPANDREDIR.store(1, SeqCst); crate::ported::subst::globlist(&mut fake, 0); IN_EXPANDREDIR.store(0, SeqCst); }
if errflag.load(SeqCst) != 0 {
return 0;
} let names: Vec<String> = fake.iter().cloned().collect();
if names.len() == 1 {
let s = crate::lex::untokenize(&names[0]); fn_.name = Some(s.clone()); if fn_.typ == REDIR_MERGEIN || fn_.typ == REDIR_MERGEOUT {
let bytes = s.as_bytes();
if bytes.len() == 1 && IS_DASH(bytes[0] as char) {
fn_.typ = REDIR_CLOSE;
} else if bytes.len() == 1 && bytes[0] == b'p' {
fn_.fd2 = -2;
} else {
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
} if i == bytes.len() && i > 0 {
fn_.fd2 = crate::ported::utils::zstrtol(&s, 10).0 as i32; } else if fn_.typ == REDIR_MERGEIN {
zerr("file number expected"); } else {
fn_.typ = REDIR_ERRWRITE; }
}
}
} else if fn_.typ == REDIR_MERGEIN {
zerr("file number expected"); } else {
if fn_.typ == REDIR_MERGEOUT {
fn_.typ = REDIR_ERRWRITE;
} for nam in names {
let mut ff = fn_.clone(); ff.name = Some(nam); redirtab.push(ff); ret = 1; }
}
ret }
#[allow(unused_variables)]
pub fn freerepldata(ptr: *mut std::ffi::c_void) { }
pub fn freematchlist(repllist: Option<&mut Vec<(usize, usize)>>) {
if let Some(l) = repllist {
l.clear(); }
}
pub fn set_pat_start(p: &str, offs: usize) -> String {
if offs == 0 || offs >= p.len() {
return p.to_string();
}
p[offs..].to_string()
}
pub fn set_pat_end(p: &str, null_me: usize) -> String {
if null_me >= p.len() {
return p.to_string();
}
p[..null_me].to_string()
}
pub fn igetmatch(
sp: &mut String,
p: &str,
fl: i32,
_n: i32, replstr: Option<&str>,
) -> i32 {
let anchored_start = (fl & SUB_START) != 0;
let anchored_end = (fl & SUB_END) != 0;
let substr_mode = (fl & SUB_SUBSTR) != 0;
let shortest = (fl & SUB_LONG) == 0;
let chars: Vec<char> = sp.chars().collect();
let len = chars.len();
if (fl & SUB_ALL) != 0 {
let i = matchpat(p, sp, true, true); if !i {
if (fl & SUB_MATCH) != 0 {
*sp = String::new();
return 0; }
return 1; }
if let Some(r) = replstr {
*sp = r.to_string();
}
if sp.is_empty() && (fl & SUB_REST) != 0 && i {
return 0; }
return 1; }
if len == 0 {
return 1;
}
if (fl & SUB_LIST) != 0 {
let mut found = false;
for start in 0..len {
for end in (start + 1)..=len {
let s2: String = chars[start..end].iter().collect();
if matchpat(p, &s2, true, true) {
found = true;
if shortest {
break;
} }
}
if !substr_mode {
break;
}
}
return if found { 0 } else { 1 };
}
let match_only = (fl & SUB_MATCH) != 0;
let (match_start, match_end) = if anchored_start && anchored_end {
if matchpat(p, sp, true, true) {
(0, len)
} else {
if match_only {
*sp = String::new();
return 0;
}
return 1;
}
} else if anchored_start {
let mut best_end = 0;
for end in 1..=len {
let substr: String = chars[..end].iter().collect();
if matchpat(p, &substr, true, true) {
if shortest {
*sp = if match_only {
chars[..end].iter().collect()
} else {
match replstr {
Some(r) => format!("{}{}", r, chars[end..].iter().collect::<String>()),
None => chars[end..].iter().collect(),
}
};
return 0;
}
best_end = end;
}
}
if best_end > 0 {
(0, best_end)
} else {
if match_only {
*sp = String::new();
return 0;
}
return 1;
}
} else if anchored_end {
let mut best_start = len;
for start in (0..len).rev() {
let substr: String = chars[start..].iter().collect();
if matchpat(p, &substr, true, true) {
if shortest {
*sp = if match_only {
chars[start..].iter().collect()
} else {
match replstr {
Some(r) => format!("{}{}", chars[..start].iter().collect::<String>(), r),
None => chars[..start].iter().collect(),
}
};
return 0;
}
best_start = start;
}
}
if best_start < len {
(best_start, len)
} else {
if match_only {
*sp = String::new();
return 0;
}
return 1;
}
} else {
for start in 0..len {
for end in (start + 1)..=len {
let substr: String = chars[start..end].iter().collect();
if matchpat(p, &substr, true, true) {
if match_only {
*sp = chars[start..end].iter().collect();
return 0;
}
let prefix: String = chars[..start].iter().collect();
let suffix: String = chars[end..].iter().collect();
*sp = match replstr {
Some(r) => format!("{}{}{}", prefix, r, suffix),
None => format!("{}{}", prefix, suffix),
};
return 0;
}
}
}
if match_only {
*sp = String::new();
return 0;
}
return 1;
};
*sp = if match_only {
chars[match_start..match_end].iter().collect()
} else {
let prefix: String = chars[..match_start].iter().collect();
let suffix: String = chars[match_end..].iter().collect();
match replstr {
Some(r) => format!("{}{}{}", prefix, r, suffix),
None => format!("{}{}", prefix, suffix),
}
};
0
}
const ZTOKENS: &str = "#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\";
pub fn tokenize(s: &mut String) {
zshtokenize(s, 0); }
pub fn shtokenize(s: &mut String) {
let mut flags = ZSHTOK_SUBST; if isset(SHGLOB) {
flags |= ZSHTOK_SHGLOB; }
zshtokenize(s, flags); }
pub fn zshtokenize(s: &mut String, flags: i32) {
let ztokens: Vec<char> = ZTOKENS.chars().collect();
let mut chars: Vec<char> = s.chars().collect();
let mut bslash = false; let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
x if x == Meta as char => { i += 2; bslash = false;
continue;
}
x if x == Bnull || x == Bnullkeep || x == '\\' => { if bslash { chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 { Bnullkeep
} else {
Bnull
};
} else {
bslash = true; i += 1;
continue; }
}
'<' => { if (flags & ZSHTOK_SHGLOB) != 0 { } else if bslash { chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 {
Bnullkeep
} else {
Bnull
};
} else {
let t = i; let mut j = i + 1;
while j < chars.len() && chars[j].is_ascii_digit() { j += 1;
}
if j < chars.len() && (chars[j] == '-') { let mut k = j + 1;
while k < chars.len() && chars[k].is_ascii_digit() { k += 1;
}
if k < chars.len() && chars[k] == '>' { chars[t] = Inang; chars[k] = Outang; i = k + 1;
bslash = false;
continue;
}
}
}
}
'(' | '|' | ')' if (flags & ZSHTOK_SHGLOB) != 0 => { }
'>' | '^' | '#' | '~' | '[' | ']' | '*' | '?' | '=' | '-' | '!' | '(' | '|' | ')' => {
for (n, &t) in ztokens.iter().enumerate() { if t == c { if bslash { chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 {
Bnullkeep
} else {
Bnull
};
} else {
chars[i] = char::from_u32(Pound as u32 + n as u32)
.unwrap_or(c); }
break; }
}
}
_ => {}
}
bslash = false; i += 1;
}
*s = chars.into_iter().collect();
}
pub fn remnulargs(s: &mut String) {
if s.is_empty() {
return;
}
let is_inull =
|c: char| c == Snull || c == Dnull || c == Bnull || c == Bnullkeep || c == Nularg;
let src: Vec<char> = s.chars().collect();
let mut out: Vec<char> = Vec::with_capacity(src.len());
let mut i = 0usize;
while i < src.len() {
let c = src[i];
if c == Bnullkeep {
i += 1;
continue;
}
if is_inull(c) {
i += 1;
while i < src.len() {
let d = src[i];
if d == Bnullkeep {
out.push('\\'); } else if !is_inull(d) {
out.push(d); }
i += 1;
}
break;
}
out.push(c);
i += 1;
}
if out.is_empty() {
out.push(Nularg);
}
*s = out.into_iter().collect();
}
#[allow(unused_variables)]
pub fn qualdev(name: &str, buf: &libc::stat, dv: i64, dummy: &str) -> i32 {
(buf.st_dev as i64 == dv) as i32 }
#[allow(unused_variables)]
pub fn qualnlink(name: &str, buf: &libc::stat, ct: i64, dummy: &str) -> i32 {
let g = G_RANGE.load(Ordering::Relaxed);
let nl = buf.st_nlink as i64; if g < 0 {
(nl < ct) as i32
} else if g > 0 {
(nl > ct) as i32
} else {
(nl == ct) as i32
}
}
#[allow(unused_variables)]
pub fn qualuid(name: &str, buf: &libc::stat, uid: i64, dummy: &str) -> i32 {
(buf.st_uid as i64 == uid) as i32 }
#[allow(unused_variables)]
pub fn qualgid(name: &str, buf: &libc::stat, gid: i64, dummy: &str) -> i32 {
(buf.st_gid as i64 == gid) as i32 }
#[allow(unused_variables)]
pub fn qualisdev(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
let m = buf.st_mode as u32 & libc::S_IFMT as u32;
((m == libc::S_IFBLK as u32) || (m == libc::S_IFCHR as u32)) as i32 }
#[allow(unused_variables)]
pub fn qualisblk(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFBLK as u32) as i32 }
#[allow(unused_variables)]
pub fn qualischr(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFCHR as u32) as i32 }
#[allow(unused_variables)]
pub fn qualisdir(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFDIR as u32) as i32 }
#[allow(unused_variables)]
pub fn qualisfifo(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFIFO as u32) as i32 }
#[allow(unused_variables)]
pub fn qualislnk(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFLNK as u32) as i32 }
#[allow(unused_variables)]
pub fn qualisreg(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32) as i32 }
#[allow(unused_variables)]
pub fn qualissock(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFSOCK as u32) as i32
}
#[allow(unused_variables)]
pub fn qualflags(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
(mode_to_octal(buf.st_mode as u32) as i64 & r#mod) as i32 }
#[allow(unused_variables)]
pub fn qualmodeflags(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
let v = mode_to_octal(buf.st_mode as u32) as i64; let y = r#mod & 0o7777;
let n = r#mod >> 12;
(((v & y) == y) && (v & n) == 0) as i32 }
#[allow(unused_variables)]
pub fn qualiscom(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
let is_reg = (buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32;
let s_ixugo: u32 = (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u32;
(is_reg && (buf.st_mode as u32 & s_ixugo) != 0) as i32 }
pub static g_units: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static g_range: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static g_amc: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
#[allow(non_snake_case)]
pub fn qualsize(_name: &str, buf: &std::fs::Metadata, size: i64, _dummy: &str) -> bool {
use std::os::unix::fs::MetadataExt;
use std::sync::atomic::Ordering;
let mut scaled: i64 = buf.size() as i64;
match g_units.load(Ordering::SeqCst) {
x if x == TT_POSIX_BLOCKS => {
scaled = (scaled + 511) / 512; }
x if x == TT_KILOBYTES => {
scaled = (scaled + 1023) / 1024; }
x if x == TT_MEGABYTES => {
scaled = (scaled + 1_048_575) / 1_048_576; }
x if x == TT_GIGABYTES => {
scaled = (scaled + 1_073_741_823) / 1_073_741_824; }
x if x == TT_TERABYTES => {
scaled = (scaled + 1_099_511_627_775) / 1_099_511_627_776; }
_ => {} }
let r = g_range.load(Ordering::SeqCst);
if r < 0 {
scaled < size
} else if r > 0 {
scaled > size
} else {
scaled == size
}
}
#[allow(non_snake_case)]
pub fn qualtime(_name: &str, buf: &std::fs::Metadata, days: i64, _dummy: &str) -> bool {
use std::os::unix::fs::MetadataExt;
use std::sync::atomic::Ordering;
let now: i64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let amc = g_amc.load(Ordering::SeqCst);
let stamp: i64 = match amc {
0 => buf.atime(), 1 => buf.mtime(), _ => buf.ctime(), };
let mut diff: i64 = now - stamp;
match g_units.load(Ordering::SeqCst) {
x if x == TT_DAYS => diff /= 86400, x if x == TT_HOURS => diff /= 3600, x if x == TT_MINS => diff /= 60, x if x == TT_WEEKS => diff /= 604800, x if x == TT_MONTHS => diff /= 2592000, _ => {} }
let r = g_range.load(Ordering::SeqCst);
if r < 0 {
diff < days
} else if r > 0 {
diff > days
} else {
diff == days
}
}
pub fn qualsheval(filename: &str, expr: &str) -> bool {
let saved_errflag = errflag.load(Ordering::Relaxed); let saved_lastval = LASTVAL.load(Ordering::Relaxed); crate::ported::params::unsetparam("reply"); crate::ported::params::setsparam("REPLY", filename); let rc = crate::ported::exec_hooks::execute_script(expr).unwrap_or(1); let ret = LASTVAL.load(Ordering::Relaxed); let _ = rc;
let post_errflag = errflag.load(Ordering::Relaxed);
errflag.store(
saved_errflag | (post_errflag & ERRFLAG_INT),
Ordering::Relaxed,
); LASTVAL.store(saved_lastval, Ordering::Relaxed); ret == 0
}
pub fn qualnonemptydir(name: &str, buf: &libc::stat, days: i64, str: &str) -> i32 {
if (buf.st_mode as u32 & libc::S_IFMT as u32) != libc::S_IFDIR as u32 {
return 0;
}
match fs::read_dir(name) {
Ok(entries) => entries.filter_map(|e| e.ok()).any(|e| {
let n = e.file_name();
let s = n.to_string_lossy();
s != "." && s != ".."
}) as i32,
Err(_) => 0,
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GlobOptSnapshot {
pub bareglobqual: bool,
pub braceccl: bool,
pub caseglob: bool,
pub extendedglob: bool,
pub globdots: bool,
pub globstarshort: bool,
pub listtypes: bool,
pub markdirs: bool,
pub nullglob: bool,
pub numericglobsort: bool,
}
pub struct GlobOptsGuard {
populated: bool,
}
#[derive(Debug, Clone)]
#[allow(non_camel_case_types)]
pub enum qualifier {
IsRegular,
IsDirectory,
IsSymlink,
IsSocket,
IsFifo,
IsBlockDev,
IsCharDev,
IsDevice,
IsExecutable,
Readable,
Writable,
Executable,
WorldReadable,
WorldWritable,
WorldExecutable,
GroupReadable,
GroupWritable,
GroupExecutable,
Setuid,
Setgid,
Sticky,
OwnedByEuid,
OwnedByEgid,
OwnedByUid(u32),
OwnedByGid(u32),
Size {
value: u64,
unit: i32,
op: char,
},
Links {
value: u64,
op: char,
},
Atime {
value: i64,
unit: i32,
op: char,
},
Mtime {
value: i64,
unit: i32,
op: char,
},
Ctime {
value: i64,
unit: i32,
op: char,
},
Mode {
yes: u32,
no: u32,
},
Device(u64),
NonEmptyDir,
Eval(String),
}
#[derive(Debug, Clone, Default)]
#[allow(non_camel_case_types)]
pub struct qualifier_set {
pub qualifiers: Vec<qualifier>,
pub alternatives: Vec<Vec<qualifier>>,
pub negated: bool,
pub follow_links: bool,
pub sorts: Vec<i32>, pub first: Option<i32>,
pub last: Option<i32>,
pub colon_mods: Option<String>,
pub pre_words: Vec<String>,
pub post_words: Vec<String>,
pub mark_dirs: bool,
pub list_types: bool,
pub nullglob: bool,
}
#[derive(Debug, Clone)]
enum PatternComponent {
Pattern(String),
Recursive { follow_links: bool },
}
pub fn enter_glob_scope() -> GlobOptsGuard {
let already = GLOB_OPTS_TLS.with_borrow(|g| g.is_some());
if already {
return GlobOptsGuard { populated: false };
}
let snap = GlobOptSnapshot::capture();
GLOB_OPTS_TLS.with_borrow_mut(|g| *g = Some(snap));
GlobOptsGuard { populated: true }
}
#[inline]
pub fn glob_isset(opt: i32) -> bool {
GLOB_OPTS_TLS.with_borrow(|g| match g {
Some(snap) => match opt {
x if x == BAREGLOBQUAL => snap.bareglobqual,
x if x == BRACECCL => snap.braceccl,
x if x == CASEGLOB => snap.caseglob,
x if x == EXTENDEDGLOB => snap.extendedglob,
x if x == GLOBDOTS => snap.globdots,
x if x == GLOBSTARSHORT => snap.globstarshort,
x if x == LISTTYPES => snap.listtypes,
x if x == MARKDIRS => snap.markdirs,
x if x == NULLGLOB => snap.nullglob,
x if x == NUMERICGLOBSORT => snap.numericglobsort,
_ => isset(opt),
},
None => isset(opt),
})
}
pub fn globdata_glob(state: &mut globdata, pattern: &str) -> Vec<String> {
let brace_ccl = glob_isset(BRACECCL);
if hasbraces(pattern, brace_ccl) {
let mut all = Vec::new();
for variant in xpandbraces(pattern, brace_ccl) {
all.extend(globdata_glob(state, &variant));
}
return all;
}
state.matches.clear();
state.pathbuf.clear();
state.pathpos = 0;
let (pat, quals) = parse_qualifiers(pattern);
state.qualifiers = quals;
if !haswilds(&pat) && state.qualifiers.is_none() {
return vec![pattern.to_string()];
}
if let Some(complist) = parse_pattern(&pat) {
if pat.starts_with('/') {
state.pathbuf.push('/');
state.pathpos = 1;
}
scanner(state, &complist, 0);
}
sort_matches(state);
apply_selection(state);
let mark_dirs = glob_isset(MARKDIRS)
|| state
.qualifiers
.as_ref()
.map(|q| q.mark_dirs)
.unwrap_or(false);
let list_types = state
.qualifiers
.as_ref()
.map(|q| q.list_types)
.unwrap_or(false);
let colon_mods = state.qualifiers.as_ref().and_then(|q| q.colon_mods.clone());
let mut results: Vec<String> = state
.matches
.iter()
.map(|m| {
let mut s = glob_emit_path(&m.path);
if mark_dirs || list_types {
if let Ok(meta) = fs::symlink_metadata(&m.path) {
let ch = file_type(meta.mode());
if list_types || (mark_dirs && ch == '/') {
s.push(ch);
}
}
}
if let Some(ref m) = colon_mods {
s = crate::ported::subst::modify(&s, m);
}
s
})
.collect();
let _ = state.qualifiers.as_ref().map(|q| q.nullglob);
results
}
pub mod qualifiers {
use std::os::unix::fs::MetadataExt;
pub fn is_regular(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| m.is_file())
.unwrap_or(false)
}
pub fn is_directory(path: &str) -> bool {
std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
}
pub fn is_symlink(path: &str) -> bool {
std::fs::symlink_metadata(path)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
pub fn is_fifo(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFIFO as u32)
.unwrap_or(false)
}
pub fn is_socket(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFSOCK as u32)
.unwrap_or(false)
}
pub fn is_block_device(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFBLK as u32)
.unwrap_or(false)
}
pub fn is_char_device(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFCHR as u32)
.unwrap_or(false)
}
pub fn is_setuid(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_ISUID as u32) != 0)
.unwrap_or(false)
}
pub fn is_setgid(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_ISGID as u32) != 0)
.unwrap_or(false)
}
pub fn is_sticky(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & libc::S_ISVTX as u32) != 0)
.unwrap_or(false)
}
pub fn is_readable(path: &str) -> bool {
std::fs::metadata(path).is_ok() && std::fs::File::open(path).is_ok()
}
pub fn is_writable(path: &str) -> bool {
std::fs::OpenOptions::new().write(true).open(path).is_ok()
}
pub fn is_executable(path: &str) -> bool {
std::fs::metadata(path)
.map(|m| (m.mode() & 0o111) != 0)
.unwrap_or(false)
}
pub fn size_matches(path: &str, size: u64, cmp: std::cmp::Ordering) -> bool {
std::fs::metadata(path)
.map(|m| m.len().cmp(&size) == cmp)
.unwrap_or(false)
}
pub fn mtime_matches(path: &str, secs: i64, cmp: std::cmp::Ordering) -> bool {
std::fs::metadata(path)
.and_then(|m| m.modified())
.map(|t| {
let elapsed = t.elapsed().map(|d| d.as_secs() as i64).unwrap_or(0);
elapsed.cmp(&secs) == cmp
})
.unwrap_or(false)
}
pub fn uid_matches(path: &str, uid: u32) -> bool {
std::fs::metadata(path)
.map(|m| m.uid() == uid)
.unwrap_or(false)
}
pub fn gid_matches(path: &str, gid: u32) -> bool {
std::fs::metadata(path)
.map(|m| m.gid() == gid)
.unwrap_or(false)
}
pub fn nlinks_matches(path: &str, nlinks: u64, cmp: std::cmp::Ordering) -> bool {
std::fs::metadata(path)
.map(|m| m.nlink().cmp(&nlinks) == cmp)
.unwrap_or(false)
}
pub fn is_command(path: &str) -> bool {
let meta = match std::fs::metadata(path) {
Ok(m) => m,
Err(_) => return false,
};
if !meta.is_file() {
return false;
}
let mode = meta.mode();
if mode & 0o111 == 0 {
return false;
}
true
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone)]
pub struct imatchdata {
pub str: String,
pub pattern: String,
pub match_start: usize,
pub match_end: usize,
pub replacement: Option<String>,
}
pub fn parse_qualifiers(pattern: &str) -> (String, Option<qualifier_set>) {
if !pattern.ends_with(')') {
return (pattern.to_string(), None);
}
let bytes = pattern.as_bytes();
let mut depth = 0;
let mut qual_start = None;
for i in (0..bytes.len()).rev() {
match bytes[i] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
qual_start = Some(i);
break;
}
}
_ => {}
}
}
let start = match qual_start {
Some(s) => s,
None => return (pattern.to_string(), None),
};
let qual_str = &pattern[start + 1..pattern.len() - 1];
let (is_explicit, qual_content) = if let Some(after) = qual_str.strip_prefix("#q") {
(true, after)
} else if glob_isset(BAREGLOBQUAL) {
(false, qual_str)
} else {
return (pattern.to_string(), None);
};
if !is_explicit && (qual_content.contains('|') || qual_content.contains('~')) {
return (pattern.to_string(), None);
}
let qs = parse_qualifier_string(qual_content);
(pattern[..start].to_string(), Some(qs))
}
fn parse_qualifier_string(s: &str) -> qualifier_set {
let mut qs = qualifier_set::default();
let mut chars = s.chars().peekable();
let mut negated = false;
let mut follow = false;
while let Some(c) = chars.next() {
match c {
'^' => negated = !negated,
'-' => follow = !follow,
',' => {
if !qs.qualifiers.is_empty() {
qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
}
negated = false;
follow = false;
}
':' => {
let rest: String = chars.collect();
qs.colon_mods = Some(format!(":{}", rest));
break;
}
'/' => qs.qualifiers.push(qualifier::IsDirectory),
'.' => qs.qualifiers.push(qualifier::IsRegular),
'@' => qs.qualifiers.push(qualifier::IsSymlink),
'=' => qs.qualifiers.push(qualifier::IsSocket),
'p' => qs.qualifiers.push(qualifier::IsFifo),
'%' => match chars.peek() {
Some('b') => {
chars.next();
qs.qualifiers.push(qualifier::IsBlockDev);
}
Some('c') => {
chars.next();
qs.qualifiers.push(qualifier::IsCharDev);
}
_ => qs.qualifiers.push(qualifier::IsDevice),
},
'*' => qs.qualifiers.push(qualifier::IsExecutable),
'r' => qs.qualifiers.push(qualifier::Readable),
'w' => qs.qualifiers.push(qualifier::Writable),
'x' => qs.qualifiers.push(qualifier::Executable),
'R' => qs.qualifiers.push(qualifier::WorldReadable),
'W' => qs.qualifiers.push(qualifier::WorldWritable),
'X' => qs.qualifiers.push(qualifier::WorldExecutable),
'A' => qs.qualifiers.push(qualifier::GroupReadable),
'I' => qs.qualifiers.push(qualifier::GroupWritable),
'E' => qs.qualifiers.push(qualifier::GroupExecutable),
's' => qs.qualifiers.push(qualifier::Setuid),
'S' => qs.qualifiers.push(qualifier::Setgid),
't' => qs.qualifiers.push(qualifier::Sticky),
'U' => qs.qualifiers.push(qualifier::OwnedByEuid),
'G' => qs.qualifiers.push(qualifier::OwnedByEgid),
'u' => {
let uid = parse_uid_gid(&mut chars);
qs.qualifiers.push(qualifier::OwnedByUid(uid));
}
'g' => {
let gid = parse_uid_gid(&mut chars);
qs.qualifiers.push(qualifier::OwnedByGid(gid));
}
'L' => {
let (unit, op, val) = parse_size_spec(&mut chars);
qs.qualifiers.push(qualifier::Size {
value: val,
unit,
op,
});
}
'l' => {
let (op, val) = parse_range_spec(&mut chars);
qs.qualifiers.push(qualifier::Links { value: val, op });
}
'a' => {
let (unit, op, val) = schedgetfn(&mut chars);
qs.qualifiers.push(qualifier::Atime {
value: val as i64,
unit,
op,
});
}
'm' => {
let (unit, op, val) = schedgetfn(&mut chars);
qs.qualifiers.push(qualifier::Mtime {
value: val as i64,
unit,
op,
});
}
'c' => {
let (unit, op, val) = schedgetfn(&mut chars);
qs.qualifiers.push(qualifier::Ctime {
value: val as i64,
unit,
op,
});
}
'o' | 'O' => {
let desc = c == 'O';
if let Some(&sc) = chars.peek() {
let key: i32 = match sc {
'n' => {
chars.next();
GS_NAME
}
'L' => {
chars.next();
GS_SIZE
}
'l' => {
chars.next();
GS_LINKS
}
'a' => {
chars.next();
GS_ATIME
}
'm' => {
chars.next();
GS_MTIME
}
'c' => {
chars.next();
GS_CTIME
}
'd' => {
chars.next();
GS_DEPTH
}
'N' => {
chars.next();
GS_NONE
}
_ => GS_NAME,
};
let shifted = if follow && (key & GS_NORMAL) != 0 {
key << GS_SHIFT
} else {
key
};
let tp = shifted | (if desc { GS_DESC } else { 0 });
qs.sorts.push(tp);
}
}
'N' => qs.nullglob = !negated,
'D' => { }
'n' => { }
'M' => qs.mark_dirs = !negated,
'T' => qs.list_types = !negated,
'F' => qs.qualifiers.push(qualifier::NonEmptyDir),
'[' => {
let (first, last) = parse_subscript(&mut chars);
qs.first = first;
qs.last = last;
}
_ => {}
}
}
if !qs.qualifiers.is_empty() {
qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
}
qs.negated = negated;
qs.follow_links = follow;
qs
}
fn parse_uid_gid(chars: &mut std::iter::Peekable<std::str::Chars>) -> u32 {
if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
let mut num = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
num.push(c);
chars.next();
} else {
break;
}
}
num.parse().unwrap_or(0)
} else {
0
}
}
fn parse_size_spec(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> (i32, char, u64) {
let unit: i32 = match chars.peek() {
Some('p') | Some('P') => {
chars.next();
TT_POSIX_BLOCKS
}
Some('k') | Some('K') => {
chars.next();
TT_KILOBYTES
}
Some('m') | Some('M') => {
chars.next();
TT_MEGABYTES
}
Some('g') | Some('G') => {
chars.next();
TT_GIGABYTES
}
Some('t') | Some('T') => {
chars.next();
TT_TERABYTES
}
_ => TT_BYTES,
};
let (op, val) = parse_range_spec(chars);
(unit, op, val)
}
fn schedgetfn(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> (i32, char, u64) {
let unit: i32 = match chars.peek() {
Some('s') => {
chars.next();
TT_SECONDS
}
Some('m') => {
chars.next();
TT_MINS
}
Some('h') => {
chars.next();
TT_HOURS
}
Some('d') => {
chars.next();
TT_DAYS
}
Some('w') => {
chars.next();
TT_WEEKS
}
Some('M') => {
chars.next();
TT_MONTHS
}
_ => TT_DAYS,
};
let (op, val) = parse_range_spec(chars);
(unit, op, val)
}
fn parse_range_spec(chars: &mut std::iter::Peekable<std::str::Chars>) -> (char, u64) {
let op: char = match chars.peek() {
Some('+') => {
chars.next();
'>'
}
Some('-') => {
chars.next();
'<'
}
_ => '=',
};
let mut num = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
num.push(c);
chars.next();
} else {
break;
}
}
let val = num.parse().unwrap_or(0);
(op, val)
}
fn parse_subscript(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> (Option<i32>, Option<i32>) {
let mut first_str = String::new();
let mut last_str = String::new();
let mut in_last = false;
while let Some(&c) = chars.peek() {
chars.next();
if c == ']' {
break;
} else if c == ',' {
in_last = true;
} else if in_last {
last_str.push(c);
} else {
first_str.push(c);
}
}
let first = first_str.parse().ok();
let last = if in_last {
last_str.parse().ok()
} else {
first
};
(first, last)
}
fn parse_pattern(pattern: &str) -> Option<Vec<PatternComponent>> {
let mut components = Vec::new();
let mut current = String::new();
let mut chars = pattern.chars().peekable();
let mut in_bracket = false;
if chars.peek() == Some(&'/') {
chars.next();
}
while let Some(c) = chars.next() {
match c {
'/' if !in_bracket => {
if !current.is_empty() {
components.push(PatternComponent::Pattern(current.clone()));
current.clear();
}
}
'[' => {
in_bracket = true;
current.push(c);
}
']' => {
in_bracket = false;
current.push(c);
}
'*' if !in_bracket && chars.peek() == Some(&'*') => {
chars.next();
let follow = chars.peek() == Some(&'*');
if follow {
chars.next();
}
let has_slash = chars.peek() == Some(&'/');
let recursive =
has_slash || follow || glob_isset(GLOBSTARSHORT);
if has_slash {
chars.next();
}
if recursive {
if !current.is_empty() {
components.push(PatternComponent::Pattern(current.clone()));
current.clear();
}
components.push(PatternComponent::Recursive {
follow_links: follow,
});
if !has_slash && !follow && chars.peek().is_some() && chars.peek() != Some(&'/')
{
current.push('*');
}
} else {
current.push('*');
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
components.push(PatternComponent::Pattern(current));
}
if let Some(PatternComponent::Recursive { .. }) = components.last() {
components.push(PatternComponent::Pattern("*".to_string()));
}
if components.is_empty() {
None
} else {
Some(components)
}
}
fn scan_pattern(
state: &mut globdata,
base: &str,
pattern: &str,
rest: &[PatternComponent],
depth: usize,
) {
let pbcwdsav = state.pathbufcwd; let mut ds = init_dirsav(); let path_max = crate::ported::zsh_system_h::PATH_MAX;
let dir = match fs::read_dir(base) {
Ok(d) => d,
Err(_) => return,
};
for entry in dir.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let no_glob_dots = !glob_isset(GLOBDOTS);
if no_glob_dots && name.starts_with('.') && !pattern.starts_with('.') {
continue;
}
let extended_glob = glob_isset(EXTENDEDGLOB);
let case_glob = glob_isset(CASEGLOB);
if matchpat(pattern, &name, extended_glob, case_glob) {
let path = entry.path();
if rest.is_empty() {
if check_qualifiers(state, &path) {
if let Ok(meta) = fs::symlink_metadata(&path) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let (tsize, tatime, tmtime, tctime, tlinks) =
if meta.file_type().is_symlink() {
if let Ok(tm) = fs::metadata(&path) {
(tm.size(), tm.atime(), tm.mtime(), tm.ctime(), tm.nlink())
} else {
(
meta.size(),
meta.atime(),
meta.mtime(),
meta.ctime(),
meta.nlink(),
)
}
} else {
(
meta.size(),
meta.atime(),
meta.mtime(),
meta.ctime(),
meta.nlink(),
)
};
state.matches.push(gmatch {
name,
path: path.to_path_buf(),
size: meta.size(),
atime: meta.atime(),
mtime: meta.mtime(),
ctime: meta.ctime(),
links: meta.nlink(),
mode: meta.mode(),
uid: meta.uid(),
gid: meta.gid(),
dev: meta.dev(),
ino: meta.ino(),
target_size: tsize,
target_atime: tatime,
target_mtime: tmtime,
target_ctime: tctime,
target_links: tlinks,
sort_strings: Vec::new(),
});
state.matchct += 1; }
}
} else {
if pbcwdsav == state.pathbufcwd
&& name.len() + state.pathpos - state.pathbufcwd as usize >= path_max
{
let cwd_anchor = state.pathbuf.get(state.pathbufcwd as usize..).unwrap_or("");
match lchdir(cwd_anchor) {
Ok(()) => {
state.pathbufcwd = state.pathpos as i32; }
Err(_) => {
zerr("current directory lost during glob");
break;
}
}
}
if path.is_dir() {
let old_pos = state.pathbuf.len();
if !state.pathbuf.is_empty() && !state.pathbuf.ends_with('/') {
state.pathbuf.push('/');
}
state.pathbuf.push_str(&name);
state.pathpos = state.pathbuf.len();
scanner(state, rest, depth + 1);
state.pathbuf.truncate(old_pos);
state.pathpos = old_pos;
}
}
}
}
if pbcwdsav < state.pathbufcwd {
if restoredir(&mut ds) != 0 {
zerr("current directory lost during glob"); }
state.pathbufcwd = pbcwdsav; }
let _ = (depth, base); }
fn scan_recursive(
state: &mut globdata,
base: &str,
rest: &[PatternComponent],
follow_links: bool,
depth: usize,
) {
let dir = match fs::read_dir(base) {
Ok(d) => d,
Err(_) => return,
};
for entry in dir.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !glob_isset(GLOBDOTS) && name.starts_with('.') {
continue;
}
let path = entry.path();
let is_dir = if follow_links {
path.is_dir()
} else {
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
};
if is_dir {
let old_pos = state.pathbuf.len();
if !state.pathbuf.is_empty() && !state.pathbuf.ends_with('/') {
state.pathbuf.push('/');
}
state.pathbuf.push_str(&name);
scanner(state, rest, depth + 1);
let next_base = state.pathbuf.clone();
scan_recursive(state, &next_base, rest, follow_links, depth + 1);
state.pathbuf.truncate(old_pos);
}
}
}
fn check_qualifiers(state: &globdata, path: &Path) -> bool {
let qs = match &state.qualifiers {
Some(q) => q,
None => return true,
};
if qs.alternatives.is_empty() {
return true;
}
let meta = match if qs.follow_links {
fs::metadata(path)
} else {
fs::symlink_metadata(path)
} {
Ok(m) => m,
Err(_) => return false,
};
for alt in &qs.alternatives {
if check_qualifier_list(alt, path, &meta) {
return !qs.negated;
}
}
qs.negated
}
fn check_qualifier_list(quals: &[qualifier], path: &Path, meta: &Metadata) -> bool {
for q in quals {
if !check_single_qualifier(q, path, meta) {
return false;
}
}
true
}
fn check_single_qualifier(qual: &qualifier, path: &Path, meta: &Metadata) -> bool {
let mode = meta.mode();
let ft = meta.file_type();
match qual {
qualifier::IsRegular => ft.is_file(),
qualifier::IsDirectory => ft.is_dir(),
qualifier::IsSymlink => ft.is_symlink(),
qualifier::IsSocket => mode & libc::S_IFMT as u32 == libc::S_IFSOCK as u32,
qualifier::IsFifo => mode & libc::S_IFMT as u32 == libc::S_IFIFO as u32,
qualifier::IsBlockDev => mode & libc::S_IFMT as u32 == libc::S_IFBLK as u32,
qualifier::IsCharDev => mode & libc::S_IFMT as u32 == libc::S_IFCHR as u32,
qualifier::IsDevice => {
let fmt = mode & libc::S_IFMT as u32;
fmt == libc::S_IFBLK as u32 || fmt == libc::S_IFCHR as u32
}
qualifier::IsExecutable => ft.is_file() && (mode & 0o111 != 0),
qualifier::Readable => mode & 0o400 != 0,
qualifier::Writable => mode & 0o200 != 0,
qualifier::Executable => mode & 0o100 != 0,
qualifier::WorldReadable => mode & 0o004 != 0,
qualifier::WorldWritable => mode & 0o002 != 0,
qualifier::WorldExecutable => mode & 0o001 != 0,
qualifier::GroupReadable => mode & 0o040 != 0,
qualifier::GroupWritable => mode & 0o020 != 0,
qualifier::GroupExecutable => mode & 0o010 != 0,
qualifier::Setuid => mode & libc::S_ISUID as u32 != 0,
qualifier::Setgid => mode & libc::S_ISGID as u32 != 0,
qualifier::Sticky => mode & libc::S_ISVTX as u32 != 0,
qualifier::OwnedByEuid => meta.uid() == unsafe { libc::geteuid() },
qualifier::OwnedByEgid => meta.gid() == unsafe { libc::getegid() },
qualifier::OwnedByUid(uid) => meta.uid() == *uid,
qualifier::OwnedByGid(gid) => meta.gid() == *gid,
qualifier::Size { value, unit, op } => {
let cmp = |a: u64, b: u64| match *op {
'<' => a < b,
'>' => a > b,
_ => a == b,
};
let size = meta.size();
let scaled = match *unit {
u if u == TT_BYTES => size,
u if u == TT_POSIX_BLOCKS => size.div_ceil(512),
u if u == TT_KILOBYTES => size.div_ceil(1024),
u if u == TT_MEGABYTES => size.div_ceil(1048576),
u if u == TT_GIGABYTES => size.div_ceil(1073741824),
u if u == TT_TERABYTES => size.div_ceil(1099511627776),
_ => size,
};
cmp(scaled, *value)
}
qualifier::Links { value, op } => {
let cmp = |a: u64, b: u64| match *op {
'<' => a < b,
'>' => a > b,
_ => a == b,
};
cmp(meta.nlink(), *value)
}
qualifier::Atime { value, unit, op } => {
let cmp = |a: i64, b: i64| match *op {
'<' => a < b,
'>' => a > b,
_ => a == b,
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let diff = now - meta.atime();
let scaled = match *unit {
u if u == TT_SECONDS => diff,
u if u == TT_MINS => diff / 60,
u if u == TT_HOURS => diff / 3600,
u if u == TT_DAYS => diff / 86400,
u if u == TT_WEEKS => diff / 604800,
u if u == TT_MONTHS => diff / 2592000,
_ => diff,
};
cmp(scaled, *value)
}
qualifier::Mtime { value, unit, op } => {
let cmp = |a: i64, b: i64| match *op {
'<' => a < b,
'>' => a > b,
_ => a == b,
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let diff = now - meta.mtime();
let scaled = match *unit {
u if u == TT_SECONDS => diff,
u if u == TT_MINS => diff / 60,
u if u == TT_HOURS => diff / 3600,
u if u == TT_DAYS => diff / 86400,
u if u == TT_WEEKS => diff / 604800,
u if u == TT_MONTHS => diff / 2592000,
_ => diff,
};
cmp(scaled, *value)
}
qualifier::Ctime { value, unit, op } => {
let cmp = |a: i64, b: i64| match *op {
'<' => a < b,
'>' => a > b,
_ => a == b,
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let diff = now - meta.ctime();
let scaled = match *unit {
u if u == TT_SECONDS => diff,
u if u == TT_MINS => diff / 60,
u if u == TT_HOURS => diff / 3600,
u if u == TT_DAYS => diff / 86400,
u if u == TT_WEEKS => diff / 604800,
u if u == TT_MONTHS => diff / 2592000,
_ => diff,
};
cmp(scaled, *value)
}
qualifier::Mode { yes, no } => {
let m = mode & 0o7777;
(m & yes) == *yes && (m & no) == 0
}
qualifier::Device(dev) => meta.dev() == *dev,
qualifier::NonEmptyDir => {
if !ft.is_dir() {
return false;
}
if let Ok(mut entries) = fs::read_dir(path) {
entries.any(|e| {
e.ok()
.map(|e| {
let name = e.file_name();
name != "." && name != ".."
})
.unwrap_or(false)
})
} else {
false
}
}
qualifier::Eval(_) => true, }
}
fn sort_matches(state: &mut globdata) {
let specs: Vec<i32> = state
.qualifiers
.as_ref()
.map(|q| {
if q.sorts.is_empty() {
vec![GS_NAME] } else {
q.sorts.clone()
}
})
.unwrap_or_else(|| vec![GS_NAME]);
if specs.iter().any(|&tp| (tp & GS_NONE) != 0) {
return;
}
let numeric = glob_isset(NUMERICGLOBSORT);
state
.matches
.sort_by(|a, b| gmatchcmp(a, b, &specs, numeric));
}
fn apply_selection(state: &mut globdata) {
let (first, last) = match &state.qualifiers {
Some(q) => (q.first, q.last),
None => return,
};
let len = state.matches.len() as i32;
if len == 0 {
return;
}
let start = match first {
Some(f) if f < 0 => (len + f).max(0) as usize,
Some(f) => (f - 1).max(0) as usize,
None => 0,
};
let end = match last {
Some(l) if l < 0 => (len + l + 1).max(0) as usize,
Some(l) => l.min(len) as usize,
None => len as usize,
};
if start < end && start < state.matches.len() {
state.matches = state.matches[start..end.min(state.matches.len())].to_vec();
} else {
state.matches.clear();
}
}
#[cfg(test)]
mod gs_tt_tests {
use super::*;
#[test]
fn gs_size_offset_matches_c() {
let _g = crate::test_util::global_state_lock();
assert_eq!(GS_SIZE, 8);
assert_eq!(GS_ATIME, 16);
assert_eq!(GS_LINKS, 128);
}
#[test]
fn gs_normal_covers_all_size_keys() {
let _g = crate::test_util::global_state_lock();
assert_ne!(GS_NORMAL & GS_SIZE, 0);
assert_ne!(GS_NORMAL & GS_ATIME, 0);
assert_ne!(GS_NORMAL & GS_MTIME, 0);
assert_ne!(GS_NORMAL & GS_CTIME, 0);
assert_ne!(GS_NORMAL & GS_LINKS, 0);
}
#[test]
fn tt_namespaces_share_indices() {
let _g = crate::test_util::global_state_lock();
assert_eq!(TT_DAYS, TT_BYTES);
assert_eq!(TT_HOURS, TT_POSIX_BLOCKS);
assert_eq!(TT_MINS, TT_KILOBYTES);
assert_eq!(TT_WEEKS, TT_MEGABYTES);
assert_eq!(TT_MONTHS, TT_GIGABYTES);
assert_eq!(TT_SECONDS, TT_TERABYTES);
}
#[test]
fn max_sorts_is_12() {
let _g = crate::test_util::global_state_lock();
assert_eq!(MAX_SORTS, 12);
}
}
fn expand_range(
prefix: &str,
content: &str,
dotdot_pos: usize,
suffix: &str,
) -> Option<Vec<String>> {
let owned: String;
let content: &str = if content.chars().any(|c| {
let cu = c as u32;
(0x84..=0xa1).contains(&cu) && c != '\u{8f}' && c != '\u{90}' && c != '\u{9a}'
}) {
owned = crate::lex::untokenize(content);
&owned
} else {
content
};
let left = &content[..dotdot_pos];
let right_start = dotdot_pos + 2;
let (right, incr_abs, incr_sign_negative, step_text) =
if let Some(pos) = content[right_start..].find("..") {
let r = &content[right_start..right_start + pos];
let s_text = &content[right_start + pos + 2..];
let raw: i64 = s_text.parse().unwrap_or(1);
if raw == 0 {
return None;
}
(r, raw.unsigned_abs(), raw < 0, s_text)
} else {
(&content[right_start..], 1u64, false, "")
};
if let (Ok(start), Ok(end)) = (left.parse::<i64>(), right.parse::<i64>()) {
let mut results = Vec::new();
let step = incr_abs.max(1) as i64;
let mut vals: Vec<i64> = Vec::new();
if start <= end {
let mut v = start;
while v <= end {
vals.push(v);
v += step;
}
} else {
let mut v = start;
while v >= end {
vals.push(v);
v -= step;
}
}
if incr_sign_negative {
vals.reverse();
}
let lstrip = left.trim_start_matches(['+', '-']);
let rstrip = right.trim_start_matches(['+', '-']);
let sstrip = step_text.trim_start_matches(['+', '-']);
let pad = lstrip.starts_with('0')
|| rstrip.starts_with('0')
|| (!step_text.is_empty() && sstrip.starts_with('0'));
let width = left.len().max(right.len()).max(step_text.len());
for v in vals {
let formatted = if pad {
if v < 0 {
let abs = (-v).to_string();
let inner_w = width.saturating_sub(1);
format!("-{:0>w$}", abs, w = inner_w)
} else {
format!("{:0>w$}", v, w = width)
}
} else {
v.to_string()
};
results.push(format!("{}{}{}", prefix, formatted, suffix));
}
return Some(results);
}
if left.len() == 1 && right.len() == 1 && step_text.is_empty() {
let start = left.chars().next()?;
let end = right.chars().next()?;
let (start, end, reverse) = if start <= end {
(start, end, false)
} else {
(end, start, true)
};
let mut results = Vec::new();
let mut chars: Vec<char> = (start..=end).collect();
if reverse {
chars.reverse();
}
for c in chars {
results.push(format!("{}{}{}", prefix, c, suffix));
}
return Some(results);
}
None
}
fn expand_comma(
prefix: &str,
content: &str,
positions: &[usize],
suffix: &str,
) -> Option<Vec<String>> {
let chars: Vec<char> = content.chars().collect();
let mut results = Vec::new();
let mut last: usize = 0;
for &pos in positions {
let part: String = chars[last..pos].iter().collect();
results.push(format!("{}{}{}", prefix, part, suffix));
last = pos + 1;
}
let tail: String = chars[last..].iter().collect();
results.push(format!("{}{}{}", prefix, tail, suffix));
Some(results)
}
fn expand_ccl(prefix: &str, content: &str, suffix: &str) -> Option<Vec<String>> {
let mut chars_set = HashSet::new();
let chars: Vec<char> = content.chars().collect();
let mut i = 0;
while i < chars.len() {
let is_range = i + 2 < chars.len() && (chars[i + 1] == '-' || chars[i + 1] == '\u{9b}');
if is_range {
let start = chars[i];
let end = chars[i + 2];
for c in start..=end {
chars_set.insert(c);
}
i += 3;
} else {
chars_set.insert(chars[i]);
i += 1;
}
}
let mut results: Vec<String> = chars_set
.iter()
.map(|c| format!("{}{}{}", prefix, c, suffix))
.collect();
results.sort();
Some(results)
}
pub fn split_qualifier(pattern: &str) -> (&str, Option<&str>) {
if !pattern.ends_with(')') {
return (pattern, None);
}
let bytes = pattern.as_bytes();
let mut depth = 0;
for i in (0..bytes.len()).rev() {
match bytes[i] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
let inner = &pattern[i + 1..pattern.len() - 1];
let inner = inner.strip_prefix("#q").unwrap_or(inner);
return (&pattern[..i], Some(inner));
}
}
_ => {}
}
}
(pattern, None)
}
fn glob_emit_path(path: &Path) -> String {
match path.components().next() {
Some(Component::Prefix(_) | Component::RootDir) => path.to_string_lossy().to_string(),
None => ".".to_string(),
_ => {
let mut out = PathBuf::new();
for c in path.components() {
match c {
Component::CurDir => {}
Component::ParentDir => out.push(".."),
Component::Normal(s) => out.push(s),
Component::Prefix(_) | Component::RootDir => {}
}
}
if out.as_os_str().is_empty() {
".".to_string()
} else {
out.to_string_lossy().to_string()
}
}
}
}
pub fn glob(pattern: &str) -> Vec<String> {
let _glob_scope = enter_glob_scope();
let mut state = globdata::new();
globdata_glob(&mut state, pattern)
}
pub fn is_directory(path: &str) -> bool {
fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
}
pub fn is_symlink(path: &str) -> bool {
fs::symlink_metadata(path)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
pub fn apply_modespec(mode: u32, who: u32, op: char, perm: u32) -> u32 {
match op {
'+' => mode | perm,
'-' => mode & !perm,
'=' => (mode & !who) | perm,
_ => mode,
}
}
pub(crate) fn expand_glob_alternation(pat: &str) -> Option<Vec<String>> {
let bytes = pat.as_bytes();
let mut i = 0;
let mut bracket_depth = 0;
let mut group_start: Option<usize> = None;
let mut group_depth = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
i += 2;
continue;
}
b'[' => bracket_depth += 1,
b']' if bracket_depth > 0 => bracket_depth -= 1,
b'(' if bracket_depth == 0 => {
if i + 1 < bytes.len() && bytes[i + 1] == b'#' {
let mut d = 1;
let mut j = i + 1;
while j < bytes.len() && d > 0 {
j += 1;
if j < bytes.len() {
match bytes[j] {
b'(' => d += 1,
b')' => d -= 1,
_ => {}
}
}
}
i = j + 1;
continue;
}
if group_start.is_none() {
group_start = Some(i);
}
group_depth += 1;
}
b')' if bracket_depth == 0 && group_depth > 0 => {
group_depth -= 1;
if group_depth == 0 {
if let Some(start) = group_start.take() {
let body = &pat[start + 1..i];
let mut bd = 0;
let mut pd = 0;
let mut found_bar = false;
for c in body.bytes() {
match c {
b'[' => bd += 1,
b']' if bd > 0 => bd -= 1,
b'(' if bd == 0 => pd += 1,
b')' if bd == 0 && pd > 0 => pd -= 1,
b'|' if bd == 0 && pd == 0 => {
found_bar = true;
break;
}
_ => {}
}
}
if found_bar {
let prefix = &pat[..start];
let suffix = &pat[i + 1..];
let mut alts: Vec<String> = Vec::new();
let mut bd2 = 0;
let mut pd2 = 0;
let mut last = 0usize;
let body_bytes = body.as_bytes();
let mut k = 0;
while k < body_bytes.len() {
let bc = body_bytes[k];
match bc {
b'[' => bd2 += 1,
b']' if bd2 > 0 => bd2 -= 1,
b'(' if bd2 == 0 => pd2 += 1,
b')' if bd2 == 0 && pd2 > 0 => pd2 -= 1,
b'|' if bd2 == 0 && pd2 == 0 => {
alts.push(format!(
"{}{}{}",
prefix,
&body[last..k],
suffix
));
last = k + 1;
}
_ => {}
}
k += 1;
}
alts.push(format!("{}{}{}", prefix, &body[last..], suffix));
return Some(alts);
}
}
}
}
_ => {}
}
i += 1;
}
None
}
pub(crate) fn find_top_level_tilde(pat: &str) -> Option<usize> {
let bytes = pat.as_bytes();
let mut i = 0;
let mut bracket_depth = 0;
let mut paren_depth = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
i += 2;
continue;
}
b'[' => bracket_depth += 1,
b']' if bracket_depth > 0 => bracket_depth -= 1,
b'(' if bracket_depth == 0 => paren_depth += 1,
b')' if bracket_depth == 0 && paren_depth > 0 => paren_depth -= 1,
b'~' if bracket_depth == 0 && paren_depth == 0 && i > 0 => {
return Some(i);
}
_ => {}
}
i += 1;
}
None
}
pub fn glob_path(pattern: &str) -> Vec<String> {
let _glob_scope = enter_glob_scope();
let opt = |n: &str, default: bool| opt_state_get(n).unwrap_or(default);
let null_glob = opt("nullglob", false);
let extended_glob = opt("extendedglob", false);
let no_glob_dots = !(opt("dotglob", false) || opt("globdots", false));
let case_glob = opt("caseglob", true) && !opt("nocaseglob", false);
if let Some(alternatives) = expand_glob_alternation(pattern) {
let mut out: Vec<String> = Vec::new();
for alt in alternatives {
let has_meta = alt.chars().any(|c| matches!(c, '*' | '?' | '[' | '('));
if has_meta {
out.extend(glob_path(&alt));
} else if Path::new(&alt).exists() {
out.push(alt);
}
}
let mut seen = HashSet::new();
out.retain(|p| seen.insert(p.clone()));
out.sort();
if !out.is_empty() {
return out;
}
}
if extended_glob {
let last_seg_start = pattern.rfind('/').map(|i| i + 1).unwrap_or(0);
let last_seg = &pattern[last_seg_start..];
if last_seg.starts_with('^') && last_seg.len() > 1 {
let prefix = &pattern[..last_seg_start];
let neg = &last_seg[1..];
let dir = if prefix.is_empty() {
".".to_string()
} else {
prefix.trim_end_matches('/').to_string()
};
let mut out = Vec::new();
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') && no_glob_dots {
continue;
}
if !matchpat(neg, &name, true, case_glob) {
let path = if prefix.is_empty() {
name
} else {
format!("{}{}", prefix, name)
};
out.push(path);
}
}
}
out.sort();
if !out.is_empty() {
return out;
}
if null_glob {
return Vec::new();
}
return vec![pattern.to_string()];
}
let chars: Vec<char> = pattern.chars().collect();
let mut depth_b = 0i32;
let mut depth_p = 0i32;
let mut split_at: Option<usize> = None;
for (i, &c) in chars.iter().enumerate() {
match c {
'[' => depth_b += 1,
']' => depth_b -= 1,
'(' => depth_p += 1,
')' => depth_p -= 1,
'~' if depth_b == 0 && depth_p == 0 && i > 0 => {
split_at = Some(i);
break;
}
_ => {}
}
}
if let Some(pos) = split_at {
let lhs: String = chars[..pos].iter().collect();
let rhs: String = chars[pos + 1..].iter().collect();
let lhs_matches = glob_path(&lhs);
let filtered: Vec<String> = lhs_matches
.into_iter()
.filter(|p| {
let basename = p.rsplit('/').next().unwrap_or(p);
!matchpat(&rhs, basename, true, case_glob)
&& !matchpat(&rhs, p, true, case_glob)
})
.collect();
if !filtered.is_empty() {
return filtered;
}
if null_glob {
return Vec::new();
}
return vec![pattern.to_string()];
}
}
let mut state = globdata::new();
let matches = globdata_glob(&mut state, pattern);
if matches.is_empty() {
let _ = parse_qualifiers(pattern); return Vec::new();
}
matches
}
pub static G_RANGE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
fn mode_to_octal(mode: u32) -> u32 {
crate::ported::utils::mode_to_octal(mode) as u32
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use tempfile::TempDir;
use crate::ported::options::{opt_state_set, opt_state_unset};
use crate::ported::zsh_h::{redir, ERRFLAG_ERROR, REDIR_WRITE};
fn tok(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => match chars.peek() {
Some('{') | Some('}') | Some(',') => {
out.push('\u{9f}');
out.push(chars.next().unwrap());
}
_ => out.push(c),
},
'{' => out.push('\u{8f}'),
'}' => out.push('\u{90}'),
',' => out.push('\u{9a}'),
_ => out.push(c),
}
}
out
}
fn setup_test_dir() -> TempDir {
let dir = TempDir::new().unwrap();
let base = dir.path();
File::create(base.join("file1.txt")).unwrap();
File::create(base.join("file2.txt")).unwrap();
File::create(base.join("file3.rs")).unwrap();
File::create(base.join(".hidden")).unwrap();
fs::create_dir(base.join("subdir")).unwrap();
File::create(base.join("subdir/nested.txt")).unwrap();
dir
}
#[test]
fn test_haswilds() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("*.txt"));
assert!(haswilds("file?.txt"));
assert!(haswilds("file[12].txt"));
assert!(!haswilds("file.txt"));
assert!(!haswilds("path/to/file.txt"));
}
#[test]
fn test_pattern_match() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("*.txt", "file.txt", false, true));
assert!(matchpat("file?.txt", "file1.txt", false, true));
assert!(!matchpat("*.txt", "file.rs", false, true));
assert!(matchpat("file[12].txt", "file1.txt", false, true));
assert!(!matchpat("file[12].txt", "file3.txt", false, true));
}
#[test]
fn test_brace_expansion() {
let _g = crate::test_util::global_state_lock();
let result = xpandbraces(&tok("{a,b,c}"), false);
assert_eq!(result, vec!["a", "b", "c"]);
let result = xpandbraces(&tok("file{1,2,3}.txt"), false);
assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
let result = xpandbraces(&tok("{1..5}"), false);
assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
let result = xpandbraces(&tok("{a..e}"), false);
assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
}
#[test]
fn test_glob_simple() {
let _g = crate::test_util::global_state_lock();
let dir = setup_test_dir();
let pattern = format!("{}/*.txt", dir.path().display());
let mut state = globdata::new();
let results = globdata_glob(&mut state, &pattern);
assert_eq!(results.len(), 2);
assert!(results.iter().any(|s| s.ends_with("file1.txt")));
assert!(results.iter().any(|s| s.ends_with("file2.txt")));
}
#[test]
fn test_glob_hidden() {
let _g = crate::test_util::global_state_lock();
let dir = setup_test_dir();
let pattern = format!("{}/*", dir.path().display());
opt_state_set("globdots", false);
let mut state = globdata::new();
let results = globdata_glob(&mut state, &pattern);
assert!(!results.iter().any(|s| s.contains(".hidden")));
opt_state_set("globdots", true);
let mut state = globdata::new();
let results = globdata_glob(&mut state, &pattern);
assert!(results.iter().any(|s| s.contains(".hidden")));
opt_state_set("globdots", false); }
#[test]
fn test_glob_emit_path_strips_read_dir_dot_slash() {
let _g = crate::test_util::global_state_lock();
assert_eq!(glob_emit_path(Path::new("./sub")), "sub");
assert_eq!(glob_emit_path(Path::new("sub/deeper")), "sub/deeper");
assert_eq!(glob_emit_path(Path::new("././x")), "x");
assert_eq!(glob_emit_path(Path::new("../up")), "../up");
}
#[test]
fn test_file_type_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(file_type(libc::S_IFDIR as u32), '/');
assert_eq!(file_type(libc::S_IFREG as u32), ' ');
assert_eq!(file_type(libc::S_IFREG as u32 | 0o111), '*');
assert_eq!(file_type(libc::S_IFLNK as u32), '@');
}
#[test]
fn file_type_every_branch_matches_c_dispatch() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
file_type(libc::S_IFBLK as u32),
'#',
"c:2020 — S_ISBLK → '#'"
);
assert_eq!(
file_type(libc::S_IFCHR as u32),
'%',
"c:2022 — S_ISCHR → '%'"
);
assert_eq!(
file_type(libc::S_IFDIR as u32),
'/',
"c:2024 — S_ISDIR → '/'"
);
assert_eq!(
file_type(libc::S_IFIFO as u32),
'|',
"c:2026 — S_ISFIFO → '|'"
);
assert_eq!(
file_type(libc::S_IFLNK as u32),
'@',
"c:2028 — S_ISLNK → '@'"
);
assert_eq!(
file_type(libc::S_IFREG as u32),
' ',
"c:2030 — non-executable regular file → ' '"
);
assert_eq!(
file_type(libc::S_IFREG as u32 | 0o100),
'*',
"c:2030 — S_IXUSR alone is enough"
);
assert_eq!(
file_type(libc::S_IFREG as u32 | 0o010),
'*',
"c:2030 — S_IXGRP alone is enough"
);
assert_eq!(
file_type(libc::S_IFREG as u32 | 0o001),
'*',
"c:2030 — S_IXOTH alone is enough"
);
assert_eq!(
file_type(libc::S_IFSOCK as u32),
'=',
"c:2033 — S_ISSOCK → '='"
);
assert_eq!(file_type(0), '?', "c:2035 — unknown st_mode → '?'");
}
#[test]
fn test_zstrcmp_numeric() {
let _g = crate::test_util::global_state_lock();
let n = crate::zsh_h::SORTIT_NUMERICALLY as u32;
assert_eq!(zstrcmp("file1", "file2", n), std::cmp::Ordering::Less);
assert_eq!(zstrcmp("file10", "file2", n), std::cmp::Ordering::Greater);
assert_eq!(zstrcmp("file10", "file10", n), std::cmp::Ordering::Equal);
}
#[test]
fn glob_opts_snapshot_isolates_concurrent_setopt() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("bareglobqual");
opt_state_set("bareglobqual", true);
let _scope = enter_glob_scope();
assert!(
glob_isset(BAREGLOBQUAL),
"TLS snapshot reads bareglobqual=true at scope entry"
);
opt_state_set("bareglobqual", false);
assert!(
glob_isset(BAREGLOBQUAL),
"TLS snapshot must still report bareglobqual=true \
even though live store now reads false"
);
match saved {
Some(v) => opt_state_set("bareglobqual", v),
None => opt_state_unset("bareglobqual"),
}
}
#[test]
fn glob_opts_snapshot_clears_on_drop() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("nullglob");
opt_state_set("nullglob", true);
{
let _scope = enter_glob_scope();
assert!(glob_isset(NULLGLOB), "snapshot live=true → true inside");
}
opt_state_set("nullglob", false);
assert!(
!glob_isset(NULLGLOB),
"post-scope: glob_isset falls back to live store"
);
match saved {
Some(v) => opt_state_set("nullglob", v),
None => opt_state_unset("nullglob"),
}
}
#[test]
fn glob_opts_snapshot_nested_is_noop() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("extendedglob");
opt_state_set("extendedglob", true);
let _outer = enter_glob_scope();
assert!(glob_isset(EXTENDEDGLOB));
{
let _inner = enter_glob_scope();
opt_state_set("extendedglob", false);
assert!(glob_isset(EXTENDEDGLOB), "inner observes outer snapshot");
} assert!(
glob_isset(EXTENDEDGLOB),
"outer snapshot survives inner drop"
);
match saved {
Some(v) => opt_state_set("extendedglob", v),
None => opt_state_unset("extendedglob"),
}
}
#[test]
fn xpandredir_single_literal_filename_returns_zero() {
let _g = crate::test_util::global_state_lock();
let mut fn_ = redir {
typ: REDIR_WRITE,
flags: 0,
fd1: 1,
fd2: -1,
name: Some("/tmp/zshrs_test_out".to_string()),
varid: None,
here_terminator: None,
munged_here_terminator: None,
};
let mut tab: Vec<redir> = Vec::new();
let r = xpandredir(&mut fn_, &mut tab);
assert_eq!(r, 0, "literal filename → single match → ret=0");
assert_eq!(
fn_.name.as_deref(),
Some("/tmp/zshrs_test_out"),
"literal name must round-trip through prefork unchanged"
);
assert!(tab.is_empty(), "no multi-match → redirtab not appended");
}
#[test]
fn xpandredir_dash_merge_collapses_to_close() {
let _g = crate::test_util::global_state_lock();
let mut fn_ = redir {
typ: REDIR_MERGEOUT,
flags: 0,
fd1: 1,
fd2: -1,
name: Some("-".to_string()),
varid: None,
here_terminator: None,
munged_here_terminator: None,
};
let mut tab: Vec<redir> = Vec::new();
let _ = xpandredir(&mut fn_, &mut tab);
assert_eq!(
fn_.typ, REDIR_CLOSE,
"`>&-` must rewrite typ to REDIR_CLOSE"
);
}
#[test]
fn xpandredir_with_no_name_returns_zero_no_panic() {
let _g = crate::test_util::global_state_lock();
let mut fn_ = redir {
typ: REDIR_WRITE,
flags: 0,
fd1: 1,
fd2: -1,
name: None,
varid: None,
here_terminator: None,
munged_here_terminator: None,
};
let mut tab: Vec<redir> = Vec::new();
assert_eq!(xpandredir(&mut fn_, &mut tab), 0);
}
#[test]
fn in_expandredir_flag_is_zero_at_rest() {
let _g = crate::test_util::global_state_lock();
assert_eq!(IN_EXPANDREDIR.load(Ordering::SeqCst), 0);
}
#[test]
fn haswilds_respects_backslash_escape() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("*.txt"), "bare * is wild");
assert!(!haswilds(r"\*.txt"), "escaped \\* is literal — NOT wild");
assert!(!haswilds(r"\?.txt"), "escaped \\? is literal — NOT wild");
}
#[test]
fn haswilds_open_bracket_alone_is_a_wildcard() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("[abc]"), "char-class is wild");
assert!(haswilds("foo["), "even unterminated [ is wild");
}
#[test]
fn haswilds_extglob_chars_inside_bracket_dont_double_count() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("[*]"));
}
#[test]
fn haswilds_plain_text_not_wild() {
let _g = crate::test_util::global_state_lock();
assert!(!haswilds("plain"));
assert!(!haswilds(""));
assert!(!haswilds("/usr/local/bin"));
assert!(!haswilds("file.txt"));
}
#[test]
fn haswilds_extended_glob_chars_recognised() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("extendedglob", false);
assert!(!haswilds("foo#bar"), "# not wild without EXTENDEDGLOB");
assert!(!haswilds("foo^bar"), "^ not wild without EXTENDEDGLOB");
crate::ported::options::opt_state_set("extendedglob", true);
assert!(haswilds("foo#bar"), "# is extglob wild");
assert!(haswilds("foo^bar"), "^ is extglob wild");
crate::ported::options::opt_state_set("extendedglob", false);
assert!(!haswilds("~/file"), "~ is NOT a filename-generation wildcard");
}
#[test]
fn matchpat_exact_literal_matches() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("hello", "hello", false, true));
assert!(!matchpat("hello", "world", false, true));
}
#[test]
fn matchpat_case_insensitive_when_flag_clear() {
let _g = crate::test_util::global_state_lock();
assert!(
matchpat("hello", "HELLO", false, false),
"case-insensitive match must succeed across cases"
);
assert!(matchpat("FoO", "foo", false, false));
}
#[test]
fn matchpat_case_sensitive_rejects_case_different() {
let _g = crate::test_util::global_state_lock();
assert!(
!matchpat("hello", "HELLO", false, true),
"case-sensitive default must reject HELLO != hello"
);
}
#[test]
fn set_pat_start_handles_out_of_range_safely() {
let _g = crate::test_util::global_state_lock();
assert_eq!(set_pat_start("hello", 0), "hello");
assert_eq!(set_pat_start("hello", 100), "hello");
assert_eq!(set_pat_start("hello", 2), "llo");
}
#[test]
fn set_pat_end_handles_out_of_range_safely() {
let _g = crate::test_util::global_state_lock();
assert_eq!(set_pat_end("hello", 100), "hello");
assert_eq!(set_pat_end("hello", 3), "hel");
assert_eq!(set_pat_end("hello", 0), "");
}
#[test]
fn freematchlist_handles_none_safely() {
let _g = crate::test_util::global_state_lock();
freematchlist(None);
}
#[test]
fn freematchlist_clears_provided_vec() {
let _g = crate::test_util::global_state_lock();
let mut v = vec![(0, 5), (10, 15)];
freematchlist(Some(&mut v));
assert!(v.is_empty(), "freematchlist must clear the input vec");
}
#[test]
fn hasbraces_matched_pair_with_comma_or_dotdot() {
let _g = crate::test_util::global_state_lock();
assert!(
hasbraces(&tok("a{b,c}d"), false),
"c:2127 — lbrace + comma + rbrace is a brace expansion"
);
assert!(
hasbraces(&tok("file{1..3}.txt"), false),
"c:2082 — N..M range is a brace expansion"
);
assert!(
!hasbraces(&tok("{abc}"), false),
"literal braces are NOT a brace expansion without comma or dotdot"
);
assert!(
!hasbraces(&tok("{abc"), false),
"lone lbrace without matching rbrace is not brace expansion"
);
assert!(
!hasbraces(&tok("abc}"), false),
"lone rbrace is not brace expansion"
);
assert!(!hasbraces(&tok("plain"), false));
assert!(!hasbraces(&tok(""), false));
}
#[test]
fn hasbraces_brace_ccl_makes_any_pair_match() {
let _g = crate::test_util::global_state_lock();
assert!(
hasbraces(&tok("{abc}"), true),
"c:2049 — BRACECCL: non-empty pair is char-class set"
);
assert!(
hasbraces(&tok("x{q}y"), true),
"c:2049 — single-char pair counts under BRACECCL"
);
assert!(
!hasbraces(&tok("{}"), true),
"empty pair still not a brace expansion even under BRACECCL"
);
assert!(
!hasbraces(&tok("{abc}"), false),
"c:2049 — BRACECCL off → plain literal pair stays literal"
);
}
#[test]
fn hasbraces_depth_1_check_for_comma_dotdot() {
let _g = crate::test_util::global_state_lock();
assert!(
hasbraces(&tok("a{1,2}b{3,4}c"), false),
"two independent top-level pairs, first one matches at depth 1"
);
}
#[test]
fn remnulargs_matches_c_inull_handling() {
let _g = crate::test_util::global_state_lock();
let mut s = "hello".to_string();
remnulargs(&mut s);
assert_eq!(
s, "hello",
"c:3654 — no inull bytes leaves string unchanged"
);
let mut s = format!("ab{}cd", Snull);
remnulargs(&mut s);
assert_eq!(
s, "abcd",
"c:3663 — Snull triggers copy; itself stripped, rest kept"
);
let mut s = format!("ab{}c{}d", Snull, Bnullkeep);
remnulargs(&mut s);
assert_eq!(
s, "abc\\d",
"c:3666 — Bnullkeep in copy phase becomes literal '\\\\'"
);
let mut s = format!("a{}b{}c{}d", Snull, Bnull, Dnull);
remnulargs(&mut s);
assert_eq!(s, "abcd", "c:3668 — Bnull/Dnull stripped in copy phase");
let mut s = format!("{}", Snull);
remnulargs(&mut s);
assert_eq!(
s,
format!("{}", Nularg),
"c:3674 — empty result replaced by Nularg sentinel"
);
}
#[test]
fn glob_exec_string_parses_qualifier_text() {
let _g = crate::test_util::global_state_lock();
let r = glob_exec_string("myfunc rest", true);
assert!(r.is_some(), "c:1092 — identifier parse should succeed");
let (ident, _adv) = r.unwrap();
assert_eq!(
ident, "myfunc",
"c:1092 — itype_end stops at first non-IIDENT char"
);
let r = glob_exec_string(" leading-space", true);
assert!(
r.is_none(),
"c:1093-1096 — empty identifier emits zerr + returns None"
);
}
#[test]
fn qualsheval_restores_errflag_and_lastval() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
LASTVAL.store(42, Ordering::Relaxed);
let _ = qualsheval("/tmp/file", ":"); assert_eq!(
errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
0,
"c:3924 — qualsheval must restore errflag (no ERRFLAG_ERROR leak)"
);
assert_eq!(
LASTVAL.load(Ordering::Relaxed),
42,
"c:3925 — qualsheval must restore lastval to pre-call value"
);
assert_eq!(
crate::ported::params::getsparam("REPLY"),
Some("/tmp/file".to_string()),
"c:3916 — qualsheval must set $REPLY to filename"
);
}
#[test]
fn xpandredir_errflag_check_uses_logical_zero() {
let _g = crate::test_util::global_state_lock();
let saved = errflag.load(Ordering::Relaxed);
errflag.store(0, Ordering::Relaxed);
let zero_enters = errflag.load(Ordering::Relaxed) == 0;
assert!(
zero_enters,
"c:2164 — errflag==0 enters the branch (the canonical sense)"
);
errflag.store(1, Ordering::Relaxed);
let nonzero_skips = errflag.load(Ordering::Relaxed) == 0;
assert!(
!nonzero_skips,
"c:2164 — errflag!=0 skips the branch (regression of bitwise-NOT bug fix)"
);
errflag.store(saved, Ordering::Relaxed);
}
#[test]
fn matchpat_literal_no_wildcards() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("foo", "foo", false, true));
assert!(!matchpat("foo", "bar", false, true));
assert!(!matchpat("foo", "foobar", false, true));
}
#[test]
fn matchpat_star_consumes_substring() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("a*b", "ab", false, true));
assert!(matchpat("a*b", "ahellob", false, true));
assert!(!matchpat("a*b", "abc", false, true));
}
#[test]
fn matchpat_question_is_single_char() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("a?c", "abc", false, true));
assert!(matchpat("a?c", "axc", false, true));
assert!(!matchpat("a?c", "ac", false, true));
assert!(!matchpat("a?c", "abbc", false, true));
}
#[test]
fn matchpat_bracket_class_inline() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("file[abc].txt", "filea.txt", false, true));
assert!(matchpat("file[abc].txt", "fileb.txt", false, true));
assert!(!matchpat("file[abc].txt", "filed.txt", false, true));
}
#[test]
fn matchpat_case_sensitive_strict() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("Foo", "Foo", false, true));
assert!(!matchpat("Foo", "foo", false, true));
assert!(!matchpat("Foo", "FOO", false, true));
}
#[test]
fn matchpat_empty_pattern_matches_only_empty() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("", "", false, true));
assert!(!matchpat("", "x", false, true));
}
#[test]
fn matchpat_star_alone_matches_anything() {
let _g = crate::test_util::global_state_lock();
assert!(matchpat("*", "", false, true));
assert!(matchpat("*", "a", false, true));
assert!(matchpat("*", "abcdef", false, true));
}
#[test]
fn matchpat_question_alone_one_char() {
let _g = crate::test_util::global_state_lock();
assert!(!matchpat("?", "", false, true));
assert!(matchpat("?", "a", false, true));
assert!(!matchpat("?", "ab", false, true));
}
#[test]
fn haswilds_each_glob_meta() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("*"));
assert!(haswilds("?"));
assert!(haswilds("[abc]"));
assert!(haswilds("a*b"));
assert!(haswilds("a?b"));
}
#[test]
fn haswilds_plain_strings_have_no_wildcards() {
let _g = crate::test_util::global_state_lock();
assert!(!haswilds(""));
assert!(!haswilds("plain.txt"));
assert!(!haswilds("/abs/path/file.rs"));
assert!(!haswilds("./rel/file"));
}
#[test]
fn xpandbraces_three_alternatives() {
let _g = crate::test_util::global_state_lock();
assert_eq!(xpandbraces(&tok("{a,b,c}"), false), vec!["a", "b", "c"]);
}
#[test]
fn xpandbraces_with_prefix_and_suffix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("pre{x,y,z}post"), false),
vec!["prexpost", "preypost", "prezpost"]
);
}
#[test]
fn xpandbraces_numeric_range_ascending() {
let _g = crate::test_util::global_state_lock();
assert_eq!(xpandbraces(&tok("{1..5}"), false), vec!["1", "2", "3", "4", "5"]);
}
#[test]
fn xpandbraces_alpha_range_ascending() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{a..e}"), false),
vec!["a", "b", "c", "d", "e"]
);
}
#[test]
fn xpandbraces_single_alternative_passes_through_literal() {
let _g = crate::test_util::global_state_lock();
let out = xpandbraces(&tok("{a}"), false);
assert_eq!(out, vec![tok("{a}")], "zsh 5.9 returns the input verbatim (TOKEN form preserved at xpandbraces layer)");
}
#[test]
fn xpandbraces_no_braces_returns_input() {
let _g = crate::test_util::global_state_lock();
let out = xpandbraces(&tok("plain"), false);
assert_eq!(out, vec!["plain"]);
}
#[test]
fn file_type_dir_marker_is_slash() {
let _g = crate::test_util::global_state_lock();
assert_eq!(file_type(libc::S_IFDIR as u32), '/');
}
#[test]
fn file_type_regular_plain_is_space() {
let _g = crate::test_util::global_state_lock();
assert_eq!(file_type(libc::S_IFREG as u32), ' ');
}
#[test]
fn file_type_regular_executable_is_star() {
let _g = crate::test_util::global_state_lock();
for x in [0o100, 0o010, 0o001] {
assert_eq!(
file_type(libc::S_IFREG as u32 | x),
'*',
"exec bit 0o{x:o} should produce '*'"
);
}
}
#[test]
fn file_type_symlink_is_at() {
let _g = crate::test_util::global_state_lock();
assert_eq!(file_type(libc::S_IFLNK as u32), '@');
}
#[test]
fn file_type_fifo_is_pipe() {
let _g = crate::test_util::global_state_lock();
assert_eq!(file_type(libc::S_IFIFO as u32), '|');
}
#[test]
fn file_type_socket_is_equal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(file_type(libc::S_IFSOCK as u32), '=');
}
#[test]
fn xpandbraces_numeric_step_two() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{1..10..2}"), false),
vec!["1", "3", "5", "7", "9"]
);
}
#[test]
fn xpandbraces_numeric_descending_step_two() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{10..1..2}"), false),
vec!["10", "8", "6", "4", "2"]
);
}
#[test]
fn xpandbraces_numeric_descending_range() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{5..1}"), false),
vec!["5", "4", "3", "2", "1"]
);
}
#[test]
fn xpandbraces_zero_padded_numeric_range() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{01..10}"), false),
vec!["01", "02", "03", "04", "05", "06", "07", "08", "09", "10"]
);
}
#[test]
fn xpandbraces_three_digit_pad_preserved() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{001..010}"), false),
vec![
"001", "002", "003", "004", "005",
"006", "007", "008", "009", "010"
]
);
}
#[test]
fn xpandbraces_escaped_braces_remain_literal_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("\\{a,b,c\\}"), false),
vec![tok("\\{a,b,c\\}")],
"xpandbraces unit: escaped braces survive without expansion (no per-element splat)"
);
}
#[test]
fn xpandbraces_cartesian_product_two_by_two() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{a,b}{c,d}"), false),
vec!["ac", "ad", "bc", "bd"]
);
}
#[test]
fn xpandbraces_nested_braces_flatten() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{a,{b,c}}"), false),
vec!["a", "b", "c"]
);
}
#[test]
fn xpandbraces_empty_braces_remain_literal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(xpandbraces(&tok("{}"), false), vec![tok("{}")]);
}
#[test]
fn xpandbraces_cartesian_with_surrounding_text() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("a{b,c}d{e,f}"), false),
vec!["abde", "abdf", "acde", "acdf"]
);
}
#[test]
fn xpandbraces_alpha_step_unsupported_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{a..z..3}"), false),
vec![tok("{a..z..3}")],
"zsh: alpha range with step → literal (TOKEN form preserved at xpandbraces layer)"
);
}
#[test]
fn zsh_corpus_basic_brace_with_nested_range() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{1,2,{3..6},7,8}Y"), false),
vec!["X1Y", "X2Y", "X3Y", "X4Y", "X5Y", "X6Y", "X7Y", "X8Y"],
"ztst:11 — basic brace expansion with nested range",
);
}
#[test]
fn zsh_corpus_numeric_range_zero_padding() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{01..4}Y"), false),
vec!["X01Y", "X02Y", "X03Y", "X04Y"],
"ztst:32 — leading-zero padding propagates to all values",
);
}
#[test]
fn zsh_corpus_numeric_range_padding_from_rhs() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{1..04}Y"), false),
vec!["X01Y", "X02Y", "X03Y", "X04Y"],
"ztst:36 — RHS padding `04` propagates",
);
}
#[test]
fn zsh_corpus_numeric_range_no_padding_when_unspecified() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{7..12}Y"), false),
vec!["X7Y", "X8Y", "X9Y", "X10Y", "X11Y", "X12Y"],
"ztst:40 — no padding when neither end has leading zero",
);
}
#[test]
fn zsh_corpus_numeric_range_lhs_padding_propagates() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{07..12}Y"), false),
vec!["X07Y", "X08Y", "X09Y", "X10Y", "X11Y", "X12Y"],
"ztst:44 — LHS padding `07` propagates",
);
}
#[test]
fn zsh_corpus_numeric_range_max_padding_width_wins() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{7..012}Y"), false),
vec!["X007Y", "X008Y", "X009Y", "X010Y", "X011Y", "X012Y"],
"ztst:48 — widest padding width wins",
);
}
#[test]
fn zsh_corpus_numeric_range_decreasing() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{4..1}Y"), false),
vec!["X4Y", "X3Y", "X2Y", "X1Y"],
"ztst:52 — decreasing range emits in reverse",
);
}
#[test]
fn zsh_corpus_combined_braces_cross_product() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{1..4}{1..4}Y"), false),
vec![
"X11Y", "X12Y", "X13Y", "X14Y",
"X21Y", "X22Y", "X23Y", "X24Y",
"X31Y", "X32Y", "X33Y", "X34Y",
"X41Y", "X42Y", "X43Y", "X44Y",
],
"ztst:56 — combined-brace cross product",
);
}
#[test]
fn zsh_corpus_negative_numbers_in_range() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{-4..4}Y"), false),
vec![
"X-4Y", "X-3Y", "X-2Y", "X-1Y", "X0Y",
"X1Y", "X2Y", "X3Y", "X4Y",
],
"ztst:60 — negative-to-positive range",
);
}
#[test]
fn zsh_corpus_brace_descending_range_from_positive_to_negative() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{4..-4}Y"), false),
vec![
"X4Y", "X3Y", "X2Y", "X1Y", "X0Y",
"X-1Y", "X-2Y", "X-3Y", "X-4Y",
],
"ztst:64 — descending 4..-4 produces 9 values",
);
}
#[test]
fn zsh_corpus_brace_stepped_padded_descending() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{004..-4..2}Y"), false),
vec!["X004Y", "X002Y", "X000Y", "X-02Y", "X-04Y"],
"ztst:68 — stepped+padded descending",
);
}
#[test]
fn zsh_corpus_brace_step_alignment_1_to_32_step_3() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("X{1..32..3}Y"), false),
vec![
"X1Y", "X4Y", "X7Y", "X10Y", "X13Y", "X16Y",
"X19Y", "X22Y", "X25Y", "X28Y", "X31Y",
],
"ztst:76 — {{1..32..3}} step alignment",
);
}
#[test]
fn zsh_corpus_brace_char_range_simple() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("hey{a..j}there"), false),
vec![
"heyathere", "heybthere", "heycthere", "heydthere",
"heyethere", "heyfthere", "heygthere", "heyhthere",
"heyithere", "heyjthere",
],
"ztst:102 — char range a..j",
);
}
#[test]
fn zsh_corpus_brace_char_range_reverse() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("crumbs{y..p}ooh"), false),
vec![
"crumbsyooh", "crumbsxooh", "crumbswooh", "crumbsvooh",
"crumbsuooh", "crumbstooh", "crumbssooh", "crumbsrooh",
"crumbsqooh", "crumbspooh",
],
"ztst:110 — char range y..p reverse",
);
}
#[test]
fn zsh_corpus_brace_nested_with_char_range_ascii() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("gosh{1,{Z..a},2}cripes"), false),
vec![
"gosh1cripes", "goshZcripes", "gosh[cripes", "gosh\\cripes",
"gosh]cripes", "gosh^cripes", "gosh_cripes", "gosh`cripes",
"goshacripes", "gosh2cripes",
],
"ztst:106 — nested brace with ASCII char range",
);
}
#[test]
#[ignore = "ZSHRS BUG: unmatched trailing braces not preserved literally"]
fn zsh_corpus_brace_unmatched_after_matched_left_literal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
xpandbraces(&tok("{1..10}{.."), false),
vec![
"1{..", "2{..", "3{..", "4{..", "5{..",
"6{..", "7{..", "8{..", "9{..", "10{..",
],
"ztst:118 — unmatched trailing {{.. left literal",
);
}
#[test]
fn split_qualifier_no_parens_returns_input_and_none() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*.txt");
assert_eq!(head, "*.txt");
assert_eq!(qual, None);
}
#[test]
fn split_qualifier_star_paren_N_extracts_null_glob_qual() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*(N)");
assert_eq!(head, "*");
assert_eq!(qual, Some("N"));
}
#[test]
fn split_qualifier_star_paren_dot_extracts_regular_file_qual() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*(.)");
assert_eq!(head, "*");
assert_eq!(qual, Some("."));
}
#[test]
fn split_qualifier_pattern_with_qual_extracts_correctly() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*.rs(.)");
assert_eq!(head, "*.rs");
assert_eq!(qual, Some("."));
}
#[test]
fn split_qualifier_hash_q_prefix_stripped() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*(#qN)");
assert_eq!(head, "*");
assert_eq!(qual, Some("N"));
}
#[test]
fn split_qualifier_multichar_qual_with_brackets() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*(om[1])");
assert_eq!(head, "*");
assert_eq!(qual, Some("om[1]"));
}
#[test]
fn split_qualifier_multiple_groups_takes_outermost_trailing() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("(a)(b)");
assert_eq!(head, "(a)");
assert_eq!(qual, Some("b"));
}
fn first_qual(qs: &qualifier_set) -> &qualifier {
if !qs.alternatives.is_empty() && !qs.alternatives[0].is_empty() {
&qs.alternatives[0][0]
} else {
&qs.qualifiers[0]
}
}
#[test]
fn parse_qualifier_string_empty_returns_empty_set() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("");
assert!(qs.qualifiers.is_empty());
assert!(qs.alternatives.is_empty());
assert!(!qs.nullglob);
}
#[test]
fn parse_qualifier_string_slash_is_directory() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("/");
assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
}
#[test]
fn parse_qualifier_string_dot_is_regular() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string(".");
assert!(matches!(first_qual(&qs), qualifier::IsRegular));
}
#[test]
fn parse_qualifier_string_at_is_symlink() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("@");
assert!(matches!(first_qual(&qs), qualifier::IsSymlink));
}
#[test]
fn parse_qualifier_string_equals_is_socket() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("=");
assert!(matches!(first_qual(&qs), qualifier::IsSocket));
}
#[test]
fn parse_qualifier_string_p_is_fifo() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("p");
assert!(matches!(first_qual(&qs), qualifier::IsFifo));
}
#[test]
fn parse_qualifier_string_star_is_executable() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("*");
assert!(matches!(first_qual(&qs), qualifier::IsExecutable));
}
#[test]
fn parse_qualifier_string_pct_b_is_block_device() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("%b");
assert!(matches!(first_qual(&qs), qualifier::IsBlockDev));
}
#[test]
fn parse_qualifier_string_pct_c_is_char_device() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("%c");
assert!(matches!(first_qual(&qs), qualifier::IsCharDev));
}
#[test]
fn parse_qualifier_string_perm_letters_map_to_perm_qualifiers() {
let _g = crate::test_util::global_state_lock();
for (letter, expected) in [
("r", qualifier::Readable),
("w", qualifier::Writable),
("x", qualifier::Executable),
] {
let qs = parse_qualifier_string(letter);
assert_eq!(
std::mem::discriminant(first_qual(&qs)),
std::mem::discriminant(&expected),
"letter {letter:?} should map to {expected:?}"
);
}
}
#[test]
fn parse_qualifier_string_capital_U_is_owned_by_euid() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("U");
assert!(matches!(first_qual(&qs), qualifier::OwnedByEuid));
}
#[test]
fn parse_qualifier_string_capital_G_is_owned_by_egid() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("G");
assert!(matches!(first_qual(&qs), qualifier::OwnedByEgid));
}
#[test]
fn parse_qualifier_string_multiple_letters_stack_in_one_alternative() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("./");
assert_eq!(qs.alternatives.len(), 1);
assert_eq!(qs.alternatives[0].len(), 2);
assert!(matches!(qs.alternatives[0][0], qualifier::IsRegular));
assert!(matches!(qs.alternatives[0][1], qualifier::IsDirectory));
}
#[test]
fn parse_qualifier_string_comma_creates_two_alternatives() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("/,.");
assert_eq!(qs.alternatives.len(), 2, "two alternatives expected");
assert!(matches!(qs.alternatives[0][0], qualifier::IsDirectory));
assert!(matches!(qs.alternatives[1][0], qualifier::IsRegular));
}
#[test]
fn parse_qualifier_string_colon_captures_modifiers_in_field() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string(":h");
assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
}
#[test]
fn parse_qualifier_string_qualifier_then_colon_mod() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("/:h");
assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
}
#[test]
fn parse_qualifier_string_capital_N_sets_nullglob_flag() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("N");
assert!(qs.nullglob, "(N) must set nullglob");
assert!(qs.alternatives.is_empty(), "N doesn't push to qualifiers");
}
#[test]
fn parse_qualifier_string_capital_M_sets_mark_dirs_flag() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("M");
assert!(qs.mark_dirs, "(M) must set mark_dirs");
}
#[test]
fn parse_qualifier_string_capital_T_sets_list_types_flag() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("T");
assert!(qs.list_types, "(T) must set list_types");
}
#[test]
fn split_then_parse_dot_qualifier_yields_is_regular() {
let _g = crate::test_util::global_state_lock();
let (head, qual) = split_qualifier("*(.)");
assert_eq!(head, "*");
let qs = parse_qualifier_string(qual.unwrap());
assert!(matches!(first_qual(&qs), qualifier::IsRegular));
}
#[test]
fn split_then_parse_slash_qualifier_yields_is_directory() {
let _g = crate::test_util::global_state_lock();
let (_, qual) = split_qualifier("*(/)");
let qs = parse_qualifier_string(qual.unwrap());
assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
}
#[test]
fn parse_qualifier_string_oN_pushes_gs_none() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("oN");
assert_eq!(qs.sorts.len(), 1);
assert_ne!(qs.sorts[0] & GS_NONE, 0, "oN must set GS_NONE bit");
}
#[test]
fn parse_qualifier_string_on_pushes_gs_name_ascending() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("on");
assert_eq!(qs.sorts.len(), 1);
assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
assert_eq!(qs.sorts[0] & GS_DESC, 0);
}
#[test]
fn parse_qualifier_string_On_pushes_gs_name_descending() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("On");
assert_eq!(qs.sorts.len(), 1);
assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
assert_eq!(qs.sorts[0] & GS_DESC, GS_DESC);
}
#[test]
fn parse_qualifier_string_chained_sort_keys_stack() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("onOL");
assert_eq!(qs.sorts.len(), 2);
}
#[test]
fn parse_qualifier_string_Lk_plus_one_size_kilobyte() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("Lk+1");
match first_qual(&qs) {
qualifier::Size { value, unit, op } => {
assert_eq!(*value, 1);
assert_eq!(*unit, TT_KILOBYTES);
assert_eq!(*op, '>', "+ is stored as '>' (greater than)");
}
other => panic!("expected Size, got {other:?}"),
}
}
#[test]
fn parse_qualifier_string_L_minus_100_size_bytes() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("L-100");
match first_qual(&qs) {
qualifier::Size { value, unit, op } => {
assert_eq!(*value, 100);
assert_eq!(*unit, TT_BYTES);
assert_eq!(*op, '<', "- is stored as '<' (less than)");
}
other => panic!("expected Size, got {other:?}"),
}
}
#[test]
fn parse_qualifier_string_Lm_megabyte_unit() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("Lm+1");
match first_qual(&qs) {
qualifier::Size { unit, .. } => assert_eq!(*unit, TT_MEGABYTES),
other => panic!("expected Size, got {other:?}"),
}
}
#[test]
fn parse_qualifier_string_m_minus_1_day_default_unit() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("m-1");
match first_qual(&qs) {
qualifier::Mtime { value, unit, op } => {
assert_eq!(*value, 1);
assert_eq!(*unit, TT_DAYS);
assert_eq!(*op, '<');
}
other => panic!("expected Mtime, got {other:?}"),
}
}
#[test]
fn parse_qualifier_string_mh_plus_24_hours() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("mh+24");
match first_qual(&qs) {
qualifier::Mtime { value, unit, op } => {
assert_eq!(*value, 24);
assert_eq!(*unit, TT_HOURS);
assert_eq!(*op, '>');
}
other => panic!("expected Mtime, got {other:?}"),
}
}
#[test]
fn parse_qualifier_string_a_minus_7_days() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("a-7");
match first_qual(&qs) {
qualifier::Atime { value, unit, op } => {
assert_eq!(*value, 7);
assert_eq!(*unit, TT_DAYS);
assert_eq!(*op, '<');
}
other => panic!("expected Atime, got {other:?}"),
}
}
#[test]
fn parse_qualifier_string_bracket_one_sets_first_to_one() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("[1]");
assert_eq!(qs.first, Some(1));
}
#[test]
fn parse_qualifier_string_om_bracket_one_sort_and_subscript() {
let _g = crate::test_util::global_state_lock();
let qs = parse_qualifier_string("om[1]");
assert_eq!(qs.sorts.len(), 1);
assert_eq!(qs.sorts[0] & GS_MTIME, GS_MTIME);
assert_eq!(qs.first, Some(1));
}
}