use std::cmp::Ordering;
use crate::ported::utils::zerr;
use std::collections::{HashMap, HashSet};
use std::fs::{self, Metadata};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::ported::sort::zstrcmp;
use crate::ported::utils::{init_dirsav, restoredir, lchdir};
use std::path::Component;
use crate::ported::zsh_h::{SUB_END, SUB_LONG};
use crate::ported::zsh_h::{Bar, Comma, Hat, Inbrace, Inbrack, Inpar, Outbrace, Outbrack, Outpar, Pound, Quest, Star, Tilde};
use crate::ported::zsh_h::Bnullkeep;
use std::process::Command;
use crate::ported::options::opt_state_get;
#[allow(unused_imports)]
use crate::ported::exec::{
self,
};
use crate::ported::zsh_h::{
isset,
BAREGLOBQUAL, BRACECCL, CASEGLOB, EXTENDEDGLOB, GLOBDOTS,
GLOBSTARSHORT, MARKDIRS, NULLGLOB, NUMERICGLOBSORT,
};
pub const GS_NAME: i32 = 1; 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;
#[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)]
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>,
}
pub fn gmatchcmp( a: &gmatch,
b: &gmatch,
specs: &[i32],
numeric_sort: bool,
) -> 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("");
crate::ported::sort::zstrcmp(asx, bsx,
if numeric_sort { crate::zsh_h::SORTIT_NUMERICALLY as u32 } else { 0 })
} else {
Ordering::Equal };
if cmp != Ordering::Equal {
return if (tp & GS_DESC) != 0 { cmp.reverse() } else { cmp };
}
}
Ordering::Equal
}
#[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,
}
#[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, }
#[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 pathbufcwd: i32, }
impl globdata {
pub fn new() -> Self {
globdata {
matches: Vec::new(),
qualifiers: None,
pathbuf: String::with_capacity(4096),
pathpos: 0,
matchct: 0,
pathbufcwd: 0,
}
}
}
pub fn globdata_glob(state: &mut globdata, pattern: &str) -> Vec<String> { let brace_ccl = crate::ported::zsh_h::isset(crate::ported::zsh_h::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 = isset(crate::ported::zsh_h::MARKDIRS)
|| state.qualifiers.as_ref().map(|q| q.mark_dirs).unwrap_or(false);
let list_types = isset(crate::ported::zsh_h::LISTTYPES)
|| 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 = apply_colon_modifiers(&s, m);
}
s
})
.collect();
if results.is_empty()
&& !isset(crate::ported::zsh_h::NULLGLOB)
{
results.push(pattern.to_string());
}
results
}
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 isset(crate::ported::zsh_h::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' => { }
'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
|| isset(crate::ported::zsh_h::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 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);
}
}
}
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 = !isset(GLOBDOTS);
if no_glob_dots && name.starts_with('.') && !pattern.starts_with('.') {
continue;
}
let extended_glob = isset(EXTENDEDGLOB);
let case_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 !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| q.sorts.clone())
.unwrap_or_else(|| vec![GS_NAME]);
if specs.iter().any(|&tp| (tp & GS_NONE) != 0) {
return;
}
let numeric = isset(crate::ported::zsh_h::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();
}
}
#[derive(Debug, Clone)]
enum PatternComponent {
Pattern(String),
Recursive { follow_links: bool },
}
pub fn haswilds(s: &str) -> bool { let mut in_bracket = false;
let mut escape = false;
for c in s.chars() {
if escape {
escape = false;
continue;
}
match c {
'\\' => escape = true,
'[' => {
in_bracket = true;
return true; }
']' => in_bracket = false,
'*' | '?' if !in_bracket => return true,
'#' | '^' | '~' if !in_bracket => return true,
_ => {}
}
}
false
}
pub fn matchpat(pattern: &str, text: &str, extended: bool, case_sensitive: bool) -> bool { let pat = if case_sensitive {
pattern.to_string()
} else {
pattern.to_lowercase()
};
let txt = if case_sensitive {
text.to_string()
} else {
text.to_lowercase()
};
patmatch(&pat, &txt, extended)
}
fn patmatch(pattern: &str, text: &str, extended: bool) -> bool {
let mut pi = pattern.chars().peekable();
let mut ti = text.chars().peekable();
while let Some(pc) = pi.next() {
match pc {
'*' => {
if pi.peek().is_none() {
return true; }
let rest: String = pi.collect();
let mut pos = 0;
for (i, _) in text
.char_indices()
.skip(ti.clone().count().saturating_sub(text.len()))
{
if i >= pos {
if patmatch(&rest, &text[i..], extended) {
return true;
}
pos = i + 1;
}
}
return patmatch(&rest, "", extended);
}
'?' => {
if ti.next().is_none() {
return false;
}
}
'[' => {
let tc = match ti.next() {
Some(c) => c,
None => return false,
};
if !patmatchrange(&mut pi, tc) {
return false;
}
}
'#' if extended => {
continue;
}
'^' if extended => {
continue;
}
'~' if extended => {
continue;
}
'\\' => {
let escaped = pi.next();
let tc = ti.next();
if escaped != tc {
return false;
}
}
_ => {
if ti.next() != Some(pc) {
return false;
}
}
}
}
ti.peek().is_none()
}
fn patmatchrange(pi: &mut std::iter::Peekable<std::str::Chars>, tc: char) -> bool {
let mut chars_in_class = Vec::new();
let mut negate = false;
let mut first = true;
while let Some(c) = pi.next() {
if first && (c == '!' || c == '^') {
negate = true;
first = false;
continue;
}
first = false;
if c == ']' && !chars_in_class.is_empty() {
break;
}
if pi.peek() == Some(&'-') {
pi.next();
if let Some(&end) = pi.peek() {
if end != ']' {
pi.next();
for ch in c..=end {
chars_in_class.push(ch);
}
continue;
}
}
chars_in_class.push(c);
chars_in_class.push('-');
continue;
}
chars_in_class.push(c);
}
let matched = chars_in_class.contains(&tc);
if negate {
!matched
} else {
matched
}
}
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();
for i in 0..len {
match chars[i] {
'{' => {
if depth == 0 {
brace_open = Some(i);
}
depth += 1;
}
'}' 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;
}
}
',' if depth == 1 => has_comma = true,
'.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' => has_dotdot = true,
_ => {}
}
}
false
}
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 == '{')?;
let mut depth = 1;
let mut comma_positions = Vec::new();
let mut dotdot_pos = None;
for i in (start + 1)..len {
match chars[i] {
'{' => depth += 1,
'}' => {
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() {
return expand_range(&prefix, &content, dp, &suffix);
}
}
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;
}
}
',' 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
}
fn expand_range(
prefix: &str,
content: &str,
dotdot_pos: usize,
suffix: &str,
) -> Option<Vec<String>> {
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);
(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 {
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 mut results = Vec::new();
let mut last = 0;
for &pos in positions {
let part = &content[last..pos];
results.push(format!("{}{}{}", prefix, part, suffix));
last = pos + 1;
}
results.push(format!("{}{}{}", prefix, &content[last..], 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() {
if i + 2 < chars.len() && chars[i + 1] == '-' {
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)
}
fn modifier_head(s: &str) -> String {
if s.is_empty() {
return ".".to_string();
}
let bytes = s.as_bytes();
let mut end = bytes.len();
while end > 0 && bytes[end - 1] == b'/' {
end -= 1;
}
while end > 0 && bytes[end - 1] != b'/' {
end -= 1;
}
if end == 0 {
return if bytes.first() == Some(&b'/') {
"/".to_string()
} else {
".".to_string()
};
}
while end > 1 && bytes[end - 1] == b'/' {
end -= 1;
}
if end == 0 {
return "/".to_string();
}
s[..end].to_string()
}
fn modifier_tail(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let bytes = s.as_bytes();
let mut end = bytes.len();
while end > 0 && bytes[end - 1] == b'/' {
end -= 1;
}
if end == 0 {
return String::new();
}
let trimmed = &s[..end];
match trimmed.rfind('/') {
Some(i) => trimmed[i + 1..].to_string(),
None => trimmed.to_string(),
}
}
fn modifier_root(s: &str) -> String {
let bytes = s.as_bytes();
let mut i = bytes.len();
while i > 0 {
let c = bytes[i - 1];
if c == b'/' {
return s.to_string();
}
if c == b'.' {
return s[..i - 1].to_string();
}
i -= 1;
}
s.to_string()
}
fn modifier_ext(s: &str) -> String {
let bytes = s.as_bytes();
let mut i = bytes.len();
while i > 0 {
let c = bytes[i - 1];
if c == b'/' {
return String::new();
}
if c == b'.' {
return s[i..].to_string();
}
i -= 1;
}
String::new()
}
fn apply_modifier_subst(input: &str, mods_after_s: &str, global: bool) -> (String, usize) {
let chars: Vec<char> = mods_after_s.chars().collect();
if chars.is_empty() {
return (input.to_string(), 0);
}
let delim = chars[0];
let mut pat = String::new();
let mut repl = String::new();
let mut filling_repl = false;
let mut i = 1;
while i < chars.len() {
let ch = chars[i];
if ch == '\\' && i + 1 < chars.len() {
let next = chars[i + 1];
if next == delim || next == '\\' {
if filling_repl {
repl.push(next);
} else {
pat.push(next);
}
i += 2;
continue;
}
}
if ch == delim {
if !filling_repl {
filling_repl = true;
i += 1;
continue;
} else {
i += 1; break;
}
}
if filling_repl {
repl.push(ch);
} else {
pat.push(ch);
}
i += 1;
}
let consumed_bytes: usize = chars[..i].iter().map(|c| c.len_utf8()).sum();
let out = if pat.is_empty() {
input.to_string()
} else if global {
input.replace(&pat, &repl)
} else {
input.replacen(&pat, &repl, 1)
};
(out, consumed_bytes)
}
fn modifier_abs(s: &str) -> String {
let base = if s.starts_with('/') {
std::path::PathBuf::from(s)
} else {
std::env::current_dir().unwrap_or_default().join(s)
};
let mut out = std::path::PathBuf::new();
for c in base.components() {
match c {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.pop();
}
other => out.push(other),
}
}
out.to_string_lossy().to_string()
}
fn modifier_realpath(s: &str) -> String {
let base = if s.starts_with('/') {
std::path::PathBuf::from(s)
} else {
std::env::current_dir().unwrap_or_default().join(s)
};
match std::fs::canonicalize(&base) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => modifier_abs(s),
}
}
fn modifier_command(s: &str) -> String {
if s.is_empty() || s.contains('/') {
return s.to_string();
}
let path = match crate::ported::params::getsparam("PATH") {
Some(p) => p,
None => return s.to_string(),
};
for dir in path.split(':') {
if dir.is_empty() {
continue;
}
let candidate = std::path::Path::new(dir).join(s);
if let Ok(meta) = std::fs::metadata(&candidate) {
if meta.is_file() {
#[cfg(unix)]
{
if meta.permissions().mode() & 0o111 != 0 {
return candidate.to_string_lossy().to_string();
}
}
#[cfg(not(unix))]
{
return candidate.to_string_lossy().to_string();
}
}
}
}
s.to_string()
}
fn modifier_lower(s: &str) -> String {
s.to_lowercase()
}
fn modifier_upper(s: &str) -> String {
s.to_uppercase()
}
fn modifier_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
if matches!(
c,
' ' | '\t'
| '\n'
| '\''
| '"'
| '\\'
| ';'
| '&'
| '|'
| '<'
| '>'
| '('
| ')'
| '{'
| '}'
| '['
| ']'
| '*'
| '?'
| '~'
| '!'
| '#'
| '$'
| '^'
| '`'
) {
out.push('\\');
}
out.push(c);
}
out
}
fn modifier_unquote(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 {
'\\' => {
if let Some(next) = chars.next() {
out.push(next);
}
}
'\'' => {
for qc in chars.by_ref() {
if qc == '\'' {
break;
}
out.push(qc);
}
}
'"' => {
while let Some(qc) = chars.next() {
if qc == '"' {
break;
}
if qc == '\\' {
if let Some(&peek) = chars.peek() {
if matches!(peek, '"' | '\\' | '$' | '`' | '\n') {
out.push(chars.next().unwrap());
continue;
}
}
}
out.push(qc);
}
}
other => out.push(other),
}
}
out
}
pub fn apply_colon_modifiers(input: &str, mods: &str) -> String {
let mut s = input.to_string();
let bytes = mods.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b':' {
i += 1;
continue;
}
i += 1;
if i >= bytes.len() {
break;
}
let mut global = false;
if bytes[i] == b'g' {
global = true;
i += 1;
if i >= bytes.len() {
break;
}
}
match bytes[i] {
b'h' => {
s = modifier_head(&s);
i += 1;
}
b't' => {
s = modifier_tail(&s);
i += 1;
}
b'r' => {
s = modifier_root(&s);
i += 1;
}
b'e' => {
s = modifier_ext(&s);
i += 1;
}
b'a' => {
s = modifier_abs(&s);
i += 1;
}
b'A' | b'P' => {
s = modifier_realpath(&s);
i += 1;
}
b'c' => {
s = modifier_command(&s);
i += 1;
}
b'l' => {
s = modifier_lower(&s);
i += 1;
}
b'u' => {
s = modifier_upper(&s);
i += 1;
}
b'q' => {
s = modifier_quote(&s);
i += 1;
}
b'Q' => {
s = modifier_unquote(&s);
i += 1;
}
b's' | b'S' => {
i += 1;
let (out, consumed) = apply_modifier_subst(&s, &mods[i..], global);
s = out;
i += consumed;
}
_ => break,
}
}
s
}
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: &std::path::Path) -> String {
match path.components().next() {
Some(Component::Prefix(_) | Component::RootDir) => path.to_string_lossy().to_string(),
None => ".".to_string(),
_ => {
let mut out = std::path::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 mut state = globdata::new();
globdata_glob(&mut state, pattern)
}
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<std::fs::Metadata> { let full = if st.is_empty() {
if s.is_empty() {
".".to_string()
} else {
s.to_string()
}
} else {
format!("{}{}", s, st)
};
if l {
std::fs::metadata(&full).ok()
} else {
std::fs::symlink_metadata(&full).ok()
}
}
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 mindist(dir: &str, name: &str, best: &mut String, exact: bool) -> usize { let Ok(entries) = std::fs::read_dir(dir) else {
return usize::MAX;
};
let mut min_dist = usize::MAX;
for entry in entries.flatten() {
let entry_name = entry.file_name().to_string_lossy().to_string();
if exact && entry_name == name {
*best = entry_name;
return 0;
}
let dist = crate::utils::spdist(name, &entry_name, min_dist);
if dist < min_dist {
min_dist = dist;
*best = entry_name.clone();
}
}
min_dist
}
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..]))
}
pub fn qualtime(s: &str, units: char) -> Option<(i64, &str)> { let (mut num, rest) = qgetnum(s)?;
match units {
'h' => num *= 3600,
'd' => num *= 86400,
'w' => num *= 604800,
'M' => num *= 2592000,
_ => {}
}
Some((num, rest))
}
pub fn qualsize(s: &str, units: char) -> Option<(i64, &str)> {
let (mut num, rest) = qgetnum(s)?;
match units {
'k' | 'K' => num *= 1024,
'm' | 'M' => num *= 1024 * 1024,
'g' | 'G' => num *= 1024 * 1024 * 1024,
't' | 'T' => num *= 1024 * 1024 * 1024 * 1024,
'p' | 'P' => num *= 512,
_ => {}
}
Some((num, rest))
}
pub mod qualifiers {
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
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 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)> {
const SUB_START: i32 = 0x1000;
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 {
const SUB_START: i32 = 0x1000;
let anchored_start = (fl & SUB_START) != 0;
let anchored_end = (fl & SUB_END) != 0;
let shortest = (fl & SUB_LONG) == 0;
let chars: Vec<char> = sp.chars().collect();
let len = chars.len();
if len == 0 {
return sp.to_string();
}
let (match_start, match_end) = if anchored_start && anchored_end {
if matchpat(pat, sp, true, true) {
(0, len)
} else {
return sp.to_string();
}
} else if anchored_start {
let mut best_end = 0;
for end in 1..=len {
let substr: String = chars[..end].iter().collect();
if matchpat(pat, &substr, true, true) {
if shortest {
return match replstr {
Some(r) => format!("{}{}", r, chars[end..].iter().collect::<String>()),
None => chars[end..].iter().collect(),
};
}
best_end = end;
}
}
if best_end > 0 {
(0, best_end)
} else {
return sp.to_string();
}
} else if anchored_end {
let mut best_start = len;
for start in (0..len).rev() {
let substr: String = chars[start..].iter().collect();
if matchpat(pat, &substr, true, true) {
if shortest {
return match replstr {
Some(r) => format!("{}{}", chars[..start].iter().collect::<String>(), r),
None => chars[..start].iter().collect(),
};
}
best_start = start;
}
}
if best_start < len {
(best_start, len)
} else {
return sp.to_string();
}
} else {
for start in 0..len {
for end in (start + 1)..=len {
let substr: String = chars[start..end].iter().collect();
if matchpat(pat, &substr, true, true) {
let prefix: String = chars[..start].iter().collect();
let suffix: String = chars[end..].iter().collect();
return match replstr {
Some(r) => format!("{}{}{}", prefix, r, suffix),
None => format!("{}{}", prefix, suffix),
};
}
}
}
return sp.to_string();
};
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),
}
}
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(s: &str, pat: &str) -> Vec<(usize, usize)> {
let mut matches = Vec::new();
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let mut pos = 0;
while pos < len {
for end in (pos + 1)..=len {
let substr: String = chars[pos..end].iter().collect();
if matchpat(pat, &substr, true, true) {
matches.push((pos, end));
pos = end;
break;
}
}
if matches.last().map(|&(_, e)| e) != Some(pos) {
pos += 1;
}
}
matches
}
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 tokenize(s: &mut String) {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'\\' => {
if i + 1 < chars.len() {
out.push(chars[i + 1]);
i += 2;
continue;
} else {
out.push('\\');
}
}
'*' => out.push(Star),
'?' => out.push(Quest),
'[' => out.push(Inbrack),
']' => out.push(Outbrack),
'(' => out.push(Inpar),
')' => out.push(Outpar),
'|' => out.push(Bar),
'#' => out.push(Pound),
'~' => out.push(Tilde),
'^' => out.push(Hat),
'{' => out.push(Inbrace),
'}' => out.push(Outbrace),
',' => out.push(Comma),
_ => out.push(c),
}
i += 1;
}
*s = out;
}
pub fn shtokenize(s: &mut String) {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
let mut in_sq = false;
let mut in_dq = false;
while i < chars.len() {
let c = chars[i];
if in_sq {
if c == '\'' { in_sq = false; } else { out.push(c); }
i += 1;
continue;
}
if in_dq {
if c == '"' {
in_dq = false;
} else if c == '\\' && i + 1 < chars.len() {
out.push(chars[i + 1]);
i += 2;
continue;
} else {
out.push(c);
}
i += 1;
continue;
}
match c {
'\'' => in_sq = true,
'"' => in_dq = true,
'\\' => {
if i + 1 < chars.len() {
out.push(chars[i + 1]);
i += 2;
continue;
}
}
'*' => out.push(Star),
'?' => out.push(Quest),
'[' => out.push(Inbrack),
']' => out.push(Outbrack),
_ => out.push(c),
}
i += 1;
}
*s = out;
}
pub fn zshtokenize(s: &mut String, extended_glob: bool, sh_glob: bool) {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'\\' => {
if i + 1 < chars.len() {
out.push(chars[i + 1]);
i += 2;
continue;
} else {
out.push('\\');
}
}
'*' => out.push(Star),
'?' => out.push(Quest),
'[' => out.push(Inbrack),
']' => out.push(Outbrack),
'#' if extended_glob => out.push(Pound),
'^' if extended_glob => out.push(Hat),
'~' if extended_glob => out.push(Tilde),
'(' if extended_glob => out.push(Inpar),
')' if extended_glob => out.push(Outpar),
'|' if extended_glob => out.push(Bar),
'{' if !sh_glob => out.push(Inbrace),
'}' if !sh_glob => out.push(Outbrace),
',' if !sh_glob => out.push(Comma),
_ => out.push(c),
}
i += 1;
}
*s = out;
}
pub fn remnulargs(s: &mut String) {
s.retain(|c| c != '\0' && c != Bnullkeep);
}
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 apply_modespec(mode: u32, who: u32, op: char, perm: u32) -> u32 {
match op {
'+' => mode | perm,
'-' => mode & !perm,
'=' => (mode & !who) | perm,
_ => mode,
}
}
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 glob_exec_string(cmd: &str, filename: &str) -> Option<String> {
let cmd = cmd.replace("$REPLY", filename).replace("{}", filename);
let output = Command::new("sh").arg("-c").arg(&cmd).output().ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
}
pub fn qualsheval(filename: &str, expr: &str) -> bool {
let script = format!("REPLY='{}'; {}", filename.replace("'", "'\\''"), expr);
Command::new("sh")
.arg("-c")
.arg(&script)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(test)]
mod gs_tt_tests {
use super::*;
#[test]
fn gs_size_offset_matches_c() {
assert_eq!(GS_SIZE, 8);
assert_eq!(GS_ATIME, 16);
assert_eq!(GS_LINKS, 128);
}
#[test]
fn gs_normal_covers_all_size_keys() {
assert!(GS_NORMAL & GS_SIZE != 0);
assert!(GS_NORMAL & GS_ATIME != 0);
assert!(GS_NORMAL & GS_MTIME != 0);
assert!(GS_NORMAL & GS_CTIME != 0);
assert!(GS_NORMAL & GS_LINKS != 0);
}
#[test]
fn tt_namespaces_share_indices() {
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() {
assert_eq!(MAX_SORTS, 12);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use std::io::Write;
use tempfile::TempDir;
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() {
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() {
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 result = xpandbraces("{a,b,c}", false);
assert_eq!(result, vec!["a", "b", "c"]);
let result = xpandbraces("file{1,2,3}.txt", false);
assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
let result = xpandbraces("{1..5}", false);
assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
let result = xpandbraces("{a..e}", false);
assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
}
#[test]
fn test_glob_simple() {
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() {
use crate::ported::options::opt_state_set;
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() {
use std::path::Path;
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() {
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 test_zstrcmp_numeric() {
use crate::ported::sort::zstrcmp;
let n = crate::zsh_h::SORTIT_NUMERICALLY as u32;
assert_eq!(zstrcmp("file1", "file2", n), Ordering::Less);
assert_eq!(zstrcmp("file10", "file2", n), Ordering::Greater);
assert_eq!(zstrcmp("file10", "file10", n), Ordering::Equal);
}
}
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
}
#[allow(unused_variables)]
pub fn insert(s: &str, checked: i32) { }
#[allow(unused_variables)]
pub fn parsecomplist(instr: &str) -> Option<Vec<String>> { None
}
pub fn parsepat(str: &str) -> Option<Vec<String>> { parsecomplist(str) }
pub fn insert_glob_match(list: &mut Vec<String>, next: usize, data: &str) { if next <= list.len() { list.insert(next, data.to_string()); } else {
list.push(data.to_string());
}
}
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 }
#[allow(unused_variables)]
pub fn zglob(list: &mut Vec<String>, np: usize, nountok: i32) { if np >= list.len() { return; }
let pattern = list[np].clone();
let matches = glob_path(&pattern);
if matches.is_empty() {
return;
}
let mut it = matches.into_iter();
list[np] = it.next().unwrap();
let mut insert_at = np + 1;
for m in it {
list.insert(insert_at, m);
insert_at += 1;
}
}
pub fn glob_path(pattern: &str) -> Vec<String> { 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 std::path::Path::new(&alt).exists() {
out.push(alt);
}
}
let mut seen = std::collections::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) = std::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() && !null_glob {
return vec![pattern.to_string()];
}
matches
}
#[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 igetmatch(_sp: &mut String, _p: *mut std::ffi::c_void, _fl: i32, _n: i32,
_replstr: Option<&str>,
_repllistp: Option<&mut Vec<(usize, usize)>>) -> i32 {
0
}
#[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(std::sync::atomic::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 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 std::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,
}
}
pub static G_RANGE: std::sync::atomic::AtomicI32 =
std::sync::atomic::AtomicI32::new(0);
fn mode_to_octal(mode: u32) -> u32 {
mode & 0o7777
}