#![allow(non_camel_case_types)]
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::os::unix::fs::PermissionsExt;
use crate::ported::zsh_h::{BANG_TOK, DINBRACK, INBRACE_TOK, OUTBRACE_TOK, CASE, COPROC, DOLOOP, DONE, ELIF, ELSE, ZEND, ESAC, FI, FOR, FOREACH, FUNC, IF, NOCORRECT, REPEAT, SELECT, THEN, TIME, UNTIL, WHILE, TYPESET, };
pub fn hasher(str: &str) -> u32 { let mut hashval: u32 = 0;
for c in str.bytes() {
hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
}
hashval
}
pub fn histhasher(s: &str) -> u32 { let mut hashval: u32 = 0;
let mut chars = s.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
} else {
break;
}
}
while let Some(c) = chars.next() {
if c.is_whitespace() {
while let Some(&next) = chars.peek() {
if next.is_whitespace() {
chars.next();
} else {
break;
}
}
if chars.peek().is_some() {
hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(' ' as u32));
}
} else {
hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
}
}
hashval
}
pub fn histstrcmp(s1: &str, s2: &str, reduce_blanks: bool) -> std::cmp::Ordering { let s1 = s1.trim_start();
let s2 = s2.trim_start();
if reduce_blanks {
return s1.cmp(s2);
}
let mut c1 = s1.chars().peekable();
let mut c2 = s2.chars().peekable();
loop {
let ch1 = c1.peek().copied();
let ch2 = c2.peek().copied();
match (ch1, ch2) {
(None, None) => return std::cmp::Ordering::Equal,
(None, Some(c)) => {
if c.is_whitespace() {
while c2.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
c2.next();
}
if c2.peek().is_none() {
return std::cmp::Ordering::Equal;
}
}
return std::cmp::Ordering::Less;
}
(Some(c), None) => {
if c.is_whitespace() {
while c1.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
c1.next();
}
if c1.peek().is_none() {
return std::cmp::Ordering::Equal;
}
}
return std::cmp::Ordering::Greater;
}
(Some(ch1), Some(ch2)) => {
let ws1 = ch1.is_whitespace();
let ws2 = ch2.is_whitespace();
if ws1 && ws2 {
while c1.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
c1.next();
}
while c2.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
c2.next();
}
} else if ws1 {
while c1.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
c1.next();
}
if c1.peek().is_none() {
return std::cmp::Ordering::Less;
}
return std::cmp::Ordering::Less;
} else if ws2 {
while c2.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
c2.next();
}
if c2.peek().is_none() {
return std::cmp::Ordering::Greater;
}
return std::cmp::Ordering::Greater;
} else if ch1 != ch2 {
return ch1.cmp(&ch2);
} else {
c1.next();
c2.next();
}
}
}
}
}
pub use crate::ported::zsh_h::cmdnam as CmdName;
pub fn cmdnam_hashed(name: &str, path: &str) -> CmdName { CmdName {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.to_string(),
flags: HASHED as i32,
},
name: None,
cmd: Some(path.to_string()),
}
}
pub fn cmdnam_unhashed(name: &str, path_segments: Vec<String>) -> CmdName { CmdName {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.to_string(),
flags: 0,
},
name: Some(path_segments),
cmd: None,
}
}
#[derive(Debug)]
pub struct cmdnam_table {
table: HashMap<String, CmdName>,
path_checked_index: usize,
path: Vec<String>,
hash_executables_only: bool,
}
impl cmdnam_table {
pub fn new() -> Self {
Self {
table: HashMap::new(),
path_checked_index: 0,
path: Vec::new(),
hash_executables_only: false,
}
}
pub fn set_path(&mut self, path: Vec<String>) {
self.path = path;
self.path_checked_index = 0;
}
pub fn set_hash_executables_only(&mut self, value: bool) {
self.hash_executables_only = value;
}
pub fn add(&mut self, cmd: CmdName) {
self.table.insert(cmd.node.nam.clone(), cmd);
}
pub fn get(&self, name: &str) -> Option<&CmdName> {
self.table.get(name)
.filter(|c| (c.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&CmdName> {
self.table.get(name)
}
pub fn remove(&mut self, name: &str) -> Option<CmdName> {
self.table.remove(name)
}
pub fn clear(&mut self) {
self.table.clear();
self.path_checked_index = 0;
}
pub fn len(&self) -> usize {
self.table.len()
}
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn hash_dir(&mut self, dir: &str, dir_index: usize) {
if dir.starts_with('.') || dir.is_empty() {
return;
}
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
if self.table.contains_key(&name) {
continue;
}
let path = entry.path();
let should_add = if self.hash_executables_only {
#[cfg(unix)]
{
path.metadata()
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
{
path.is_file()
}
} else {
true
};
if should_add {
let segment = self.path.get(dir_index).cloned()
.unwrap_or_else(|| dir.to_string());
self.table.insert(
name.clone(),
cmdnam_unhashed(&name, vec![segment]),
);
}
}
}
pub fn fill(&mut self) {
for i in self.path_checked_index..self.path.len() {
let dir = self.path[i].clone();
self.hash_dir(&dir, i);
}
self.path_checked_index = self.path.len();
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &CmdName)> {
self.table.iter()
}
pub fn get_full_path(&self, name: &str) -> Option<PathBuf> {
let cmd = self.table.get(name)?;
if (cmd.node.flags & DISABLED as i32) != 0 {
return None;
}
if (cmd.node.flags & HASHED as i32) != 0 {
if let Some(ref s) = cmd.cmd {
return Some(PathBuf::from(s));
}
}
if let Some(ref segs) = cmd.name {
if let Some(seg) = segs.first() {
let mut path = PathBuf::from(seg);
path.push(name);
return Some(path);
}
}
None
}
}
impl Default for cmdnam_table {
fn default() -> Self {
Self::new()
}
}
pub use crate::ported::zsh_h::shfunc as ShFunc;
pub fn shfunc_with_body(name: &str, body: &str) -> ShFunc { ShFunc {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.to_string(),
flags: 0,
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: Some(body.to_string()),
}
}
pub fn shfunc_autoload(name: &str) -> ShFunc { ShFunc {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.to_string(),
flags: PM_UNDEFINED as i32,
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: None,
}
}
#[derive(Debug)]
pub struct shfunc_table {
table: HashMap<String, ShFunc>,
}
impl shfunc_table {
pub fn new() -> Self {
Self {
table: HashMap::new(),
}
}
pub fn add(&mut self, func: ShFunc) -> Option<ShFunc> {
self.table.insert(func.node.nam.clone(), func)
}
pub fn get(&self, name: &str) -> Option<&ShFunc> {
self.table
.get(name)
.filter(|f| (f.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&ShFunc> {
self.table.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut ShFunc> {
self.table
.get_mut(name)
.filter(|f| (f.node.flags & DISABLED as i32) == 0)
}
pub fn remove(&mut self, name: &str) -> Option<ShFunc> {
self.table.remove(name)
}
pub fn disable(&mut self, name: &str) -> bool {
if let Some(func) = self.table.get_mut(name) {
func.node.flags |= DISABLED as i32;
true
} else {
false
}
}
pub fn enable(&mut self, name: &str) -> bool {
if let Some(func) = self.table.get_mut(name) {
func.node.flags &= !(DISABLED as i32);
true
} else {
false
}
}
pub fn len(&self) -> usize {
self.table.len()
}
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &ShFunc)> {
self.table.iter()
}
pub fn iter_sorted(&self) -> Vec<(&String, &ShFunc)> {
let mut entries: Vec<_> = self.table.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
entries
}
pub fn clear(&mut self) {
self.table.clear();
}
}
impl Default for shfunc_table {
fn default() -> Self {
Self::new()
}
}
pub use crate::ported::zsh_h::reswd as Reswd;
#[derive(Debug)]
pub struct reswd_table {
table: HashMap<String, Reswd>,
}
impl reswd_table {
pub fn new() -> Self {
let mut table = HashMap::new();
let words: [(&str, i32); 31] = [ ("!", BANG_TOK), ("[[", DINBRACK), ("{", INBRACE_TOK), ("}", OUTBRACE_TOK), ("case", CASE), ("coproc", COPROC), ("declare", TYPESET), ("do", DOLOOP), ("done", DONE), ("elif", ELIF), ("else", ELSE), ("end", ZEND), ("esac", ESAC), ("export", TYPESET), ("fi", FI), ("float", TYPESET), ("for", FOR), ("foreach", FOREACH), ("function", FUNC), ("if", IF), ("integer", TYPESET), ("local", TYPESET), ("nocorrect", NOCORRECT), ("readonly", TYPESET), ("repeat", REPEAT), ("select", SELECT), ("then", THEN), ("time", TIME), ("typeset", TYPESET), ("until", UNTIL), ("while", WHILE), ];
for (name, token) in words {
table.insert(
name.to_string(),
Reswd {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.to_string(),
flags: 0,
},
token,
},
);
}
Self { table }
}
pub fn get(&self, name: &str) -> Option<&Reswd> {
self.table.get(name)
.filter(|r| (r.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&Reswd> {
self.table.get(name)
}
pub fn disable(&mut self, name: &str) -> bool {
if let Some(rw) = self.table.get_mut(name) {
rw.node.flags |= DISABLED as i32;
true
} else {
false
}
}
pub fn enable(&mut self, name: &str) -> bool {
if let Some(rw) = self.table.get_mut(name) {
rw.node.flags &= !(DISABLED as i32);
true
} else {
false
}
}
pub fn is_reserved(&self, name: &str) -> bool {
self.get(name).is_some()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Reswd)> {
self.table.iter()
}
}
impl Default for reswd_table {
fn default() -> Self {
Self::new()
}
}
pub use crate::ported::zsh_h::alias as Alias;
use crate::zsh_h::{ALIAS_GLOBAL, ALIAS_SUFFIX, DISABLED, HASHED, PM_LOADDIR, PM_TAGGED, PM_UNDEFINED};
pub fn createaliasnode(name: &str, text: &str, flags: u32) -> Alias { Alias {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.to_string(),
flags: flags as i32,
},
text: text.to_string(),
inuse: 0,
}
}
#[derive(Debug)]
pub struct alias_table {
table: HashMap<String, Alias>,
}
impl alias_table {
pub fn new() -> Self {
Self {
table: HashMap::new(),
}
}
pub fn with_defaults() -> Self {
let mut table = Self::new();
table.add(createaliasnode("run-help", "man", 0)); table.add(createaliasnode("which-command", "whence", 0)); table
}
pub fn add(&mut self, alias: Alias) -> Option<Alias> {
self.table.insert(alias.node.nam.clone(), alias)
}
pub fn get(&self, name: &str) -> Option<&Alias> {
self.table.get(name)
.filter(|a| (a.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&Alias> {
self.table.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut Alias> {
self.table.get_mut(name)
.filter(|a| (a.node.flags & DISABLED as i32) == 0)
}
pub fn remove(&mut self, name: &str) -> Option<Alias> {
self.table.remove(name)
}
pub fn disable(&mut self, name: &str) -> bool {
if let Some(alias) = self.table.get_mut(name) {
alias.node.flags |= DISABLED as i32;
true
} else {
false
}
}
pub fn enable(&mut self, name: &str) -> bool {
if let Some(alias) = self.table.get_mut(name) {
alias.node.flags &= !(DISABLED as i32);
true
} else {
false
}
}
pub fn len(&self) -> usize {
self.table.len()
}
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn clear(&mut self) {
self.table.clear();
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Alias)> {
self.table.iter()
}
pub fn iter_sorted(&self) -> Vec<(&String, &Alias)> {
let mut entries: Vec<_> = self.table.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
entries
}
}
impl Default for alias_table {
fn default() -> Self {
Self::new()
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone)]
pub struct dircache_entry { pub name: String, pub refs: i32, }
static DIRCACHE_INNER: std::sync::OnceLock<
std::sync::Mutex<Vec<dircache_entry>>,
> = std::sync::OnceLock::new();
static DIRCACHE_LASTENTRY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(usize::MAX);
pub fn dircache_lock() -> &'static std::sync::Mutex<Vec<dircache_entry>> {
DIRCACHE_INNER.get_or_init(|| std::sync::Mutex::new(Vec::new()))
}
pub mod print_flags {
pub const NAMEONLY: u32 = 1 << 0;
pub const WHENCE_WORD: u32 = 1 << 1;
pub const WHENCE_SIMPLE: u32 = 1 << 2;
pub const WHENCE_CSH: u32 = 1 << 3;
pub const WHENCE_VERBOSE: u32 = 1 << 4;
pub const WHENCE_FUNCDEF: u32 = 1 << 5;
pub const LIST: u32 = 1 << 6;
}
pub fn printcmdnamnode(cmd: &CmdName, _path: &[String], print_flags: u32) -> String {
let name = &cmd.node.nam;
let is_hashed = (cmd.node.flags & HASHED as i32) != 0;
let resolved = || -> Option<String> {
if is_hashed { cmd.cmd.clone() }
else { cmd.name.as_ref()
.and_then(|v| v.first())
.map(|seg| format!("{}/{}", seg, name)) }
};
if print_flags & print_flags::WHENCE_WORD != 0 {
let kind = if is_hashed { "hashed" } else { "command" };
return format!("{}: {}\n", name, kind);
}
if print_flags & (print_flags::WHENCE_CSH | print_flags::WHENCE_SIMPLE) != 0 {
if let Some(p) = resolved() {
return format!("{}\n", p);
}
return format!("{}\n", name);
}
if print_flags & print_flags::WHENCE_VERBOSE != 0 {
if is_hashed {
if let Some(p) = resolved() {
return format!("{} is hashed to {}\n", name, p);
}
} else if let Some(p) = resolved() {
return format!("{} is {}\n", name, p);
}
return format!("{} is {}\n", name, name);
}
if print_flags & print_flags::LIST != 0 {
let prefix = if name.starts_with('-') { "hash -- " } else { "hash " };
if let Some(p) = resolved() {
return format!("{}{}={}\n", prefix, name, p);
}
}
if let Some(p) = resolved() {
return format!("{}={}\n", name, p);
}
format!("{}={}\n", name, name)
}
pub fn printshfuncnode(func: &ShFunc, print_flags: u32) -> String {
let name = &func.node.nam;
if print_flags & print_flags::NAMEONLY != 0
|| (print_flags & print_flags::WHENCE_SIMPLE != 0
&& print_flags & print_flags::WHENCE_FUNCDEF == 0)
{
return format!("{}\n", name);
}
if print_flags & (print_flags::WHENCE_VERBOSE | print_flags::WHENCE_WORD) != 0
&& print_flags & print_flags::WHENCE_FUNCDEF == 0
{
if print_flags & print_flags::WHENCE_WORD != 0 {
return format!("{}: function\n", name);
}
let kind = if (func.node.flags & PM_UNDEFINED as i32) != 0 {
"is an autoload shell function"
} else {
"is a shell function"
};
let mut result = format!("{} {}", name, kind);
if let Some(ref filename) = func.filename {
result.push_str(&format!(" from {}", filename));
}
result.push('\n');
return result;
}
let mut result = format!("{} () {{\n", name);
if (func.node.flags & PM_UNDEFINED as i32) != 0 {
result.push_str("\t# undefined\n");
if (func.node.flags & PM_TAGGED as i32) != 0 {
result.push_str("\t# traced\n");
}
result.push_str("\tbuiltin autoload -X");
if let Some(ref filename) = func.filename {
if (func.node.flags & PM_LOADDIR as i32) != 0 {
result.push_str(&format!(" {}", filename));
}
}
} else if let Some(ref body) = func.body {
if (func.node.flags & PM_TAGGED as i32) != 0 {
result.push_str("\t# traced\n");
}
for line in body.lines() {
result.push_str(&format!("\t{}\n", line));
}
}
result.push_str("}\n");
result
}
pub fn format_reswd(rw: &Reswd, print_flags: u32) -> String {
let name = &rw.node.nam;
if print_flags & print_flags::WHENCE_WORD != 0 {
return format!("{}: reserved\n", name);
}
if print_flags & print_flags::WHENCE_CSH != 0 {
return format!("{}: shell reserved word\n", name);
}
if print_flags & print_flags::WHENCE_VERBOSE != 0 {
return format!("{} is a reserved word\n", name);
}
format!("{}\n", name)
}
pub fn format_alias(alias: &Alias, print_flags: u32) -> String {
let name = &alias.node.nam;
let text = &alias.text;
let af = alias.node.flags;
let is_suffix = (af & ALIAS_SUFFIX as i32) != 0;
let is_global = (af & ALIAS_GLOBAL as i32) != 0;
if print_flags & print_flags::NAMEONLY != 0 {
return format!("{}\n", name);
}
if print_flags & print_flags::WHENCE_WORD != 0 {
let kind = if is_suffix {
"suffix alias"
} else if is_global {
"global alias"
} else {
"alias"
};
return format!("{}: {}\n", name, kind);
}
if print_flags & print_flags::WHENCE_SIMPLE != 0 {
return format!("{}\n", text);
}
if print_flags & print_flags::WHENCE_CSH != 0 {
let kind = if is_suffix {
"suffix "
} else if is_global {
"globally "
} else {
""
};
return format!("{}: {}aliased to {}\n", name, kind, text);
}
if print_flags & print_flags::WHENCE_VERBOSE != 0 {
let kind = if is_suffix {
" suffix"
} else if is_global {
" global"
} else {
"n"
};
return format!("{} is a{} alias for {}\n", name, kind, text);
}
if print_flags & print_flags::LIST != 0 {
if name.contains('=') {
return format!("# invalid alias '{}'\n", name);
}
let mut result = String::from("alias ");
if is_suffix {
result.push_str("-s ");
} else if is_global {
result.push_str("-g ");
}
if name.starts_with('-') || name.starts_with('+') {
result.push_str("-- ");
}
result.push_str(&format!("{}={}\n", crate::ported::utils::quotedzputs(name), crate::ported::utils::quotedzputs(text)));
return result;
}
format!("{}={}\n", crate::ported::utils::quotedzputs(name), crate::ported::utils::quotedzputs(text))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hasher() {
assert_eq!(hasher(""), 0);
assert!(hasher("test") != 0);
assert_eq!(hasher("test"), hasher("test"));
assert_ne!(hasher("test"), hasher("Test"));
}
#[test]
fn test_histhasher() {
assert_eq!(histhasher(" hello world "), histhasher("hello world"));
assert_ne!(histhasher("hello world"), histhasher("helloworld"));
}
#[test]
fn test_histstrcmp() {
assert_eq!(
histstrcmp(" hello world ", "hello world", false),
std::cmp::Ordering::Equal
);
assert_eq!(
histstrcmp("hello world", "hello world", true),
std::cmp::Ordering::Equal
);
}
#[test]
fn test_cmdnam_table() {
let mut table = cmdnam_table::new();
table.add(cmdnam_hashed("ls", "/bin/ls"));
assert!(table.get("ls").is_some());
assert!(table.get("nonexistent").is_none());
let ls = table.get("ls").unwrap();
assert!((ls.node.flags & HASHED as i32) != 0);
assert!((ls.node.flags & DISABLED as i32) == 0);
}
#[test]
fn test_shfunc_table() {
let mut table = shfunc_table::new();
table.add(shfunc_with_body("myfunc", "echo hello"));
table.add(shfunc_autoload("lazy"));
assert!(table.get("myfunc").is_some());
assert!(
(table.get("myfunc").unwrap().node.flags & PM_UNDEFINED as i32) == 0
);
assert!(
(table.get("lazy").unwrap().node.flags & PM_UNDEFINED as i32) != 0
);
table.disable("myfunc");
assert!(table.get("myfunc").is_none());
assert!(table.get_including_disabled("myfunc").is_some());
table.enable("myfunc");
assert!(table.get("myfunc").is_some());
}
#[test]
fn test_reswd_table() {
let table = reswd_table::new();
assert!(table.is_reserved("if"));
assert!(table.is_reserved("while"));
assert!(table.is_reserved("[["));
assert!(!table.is_reserved("notreserved"));
let if_rw = table.get("if").unwrap();
assert_eq!(if_rw.token, crate::ported::zsh_h::IF);
}
#[test]
fn test_alias_table() {
let mut table = alias_table::with_defaults();
assert!(table.get("run-help").is_some());
assert_eq!(table.get("run-help").unwrap().text, "man");
table.add(createaliasnode("G", "| grep", ALIAS_GLOBAL as u32));
let g = table.get("G").unwrap();
assert!((g.node.flags & ALIAS_GLOBAL as i32) != 0);
table.add(createaliasnode("pdf", "zathura", ALIAS_SUFFIX as u32));
let p = table.get("pdf").unwrap();
assert!((p.node.flags & ALIAS_SUFFIX as i32) != 0);
table.disable("G");
assert!(table.get("G").is_none());
}
#[test]
fn test_dir_cache() {
let cache = super::dircache_lock();
{
let mut g = cache.lock().unwrap();
g.clear();
g.push(dircache_entry { name: "/usr/share/zsh".into(), refs: 1 });
g.push(dircache_entry { name: "/usr/share/zsh".into(), refs: 1 });
assert_eq!(g.len(), 2);
assert_eq!(g[0].refs, 1);
}
}
#[test]
fn test_format_alias() {
let alias = createaliasnode("ll", "ls -l", 0);
let output = format_alias(&alias, print_flags::WHENCE_VERBOSE);
assert!(output.contains("is an alias for"));
let global = createaliasnode("G", "| grep", ALIAS_GLOBAL as u32);
let output = format_alias(&global, print_flags::WHENCE_WORD);
assert!(output.contains("global alias"));
}
#[test]
fn test_format_reswd() {
let table = reswd_table::new();
let if_rw = table.get("if").unwrap();
let output = format_reswd(if_rw, print_flags::WHENCE_VERBOSE);
assert!(output.contains("is a reserved word"));
let output = format_reswd(if_rw, print_flags::WHENCE_WORD);
assert!(output.contains("reserved"));
}
static SHFUNCTAB_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn fresh_shfunctab() {
let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
tab.clear();
}
#[test]
fn test_createshfunctable_idempotent() {
let _g = SHFUNCTAB_TEST_LOCK.lock();
createshfunctable();
createshfunctable();
let h1 = shfunctab_lock() as *const _;
let h2 = shfunctab_lock() as *const _;
assert_eq!(h1, h2);
}
#[test]
fn test_shfunctab_add_get_remove() {
let _g = SHFUNCTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(shfunc_with_body("greet", "echo hello"));
}
{
let tab = shfunctab_lock().read().unwrap();
assert!(tab.get("greet").is_some());
assert_eq!(
tab.get("greet").unwrap().body.as_deref(),
Some("echo hello")
);
}
let removed = removeshfuncnode("greet");
assert!(removed.is_some());
assert!(shfunctab_lock().read().unwrap().get("greet").is_none());
}
#[test]
fn test_shfunctab_disable_enable() {
let _g = SHFUNCTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(shfunc_with_body("f", "true"));
}
disableshfuncnode("f");
{
let tab = shfunctab_lock().read().unwrap();
assert!(tab.get("f").is_none());
assert!(tab.get_including_disabled("f").is_some());
}
enableshfuncnode("f");
assert!(shfunctab_lock().read().unwrap().get("f").is_some());
removeshfuncnode("f");
}
#[test]
fn test_simple_glob_match() {
assert!(simple_glob_match("foo", "foo"));
assert!(!simple_glob_match("foo", "bar"));
assert!(simple_glob_match("f*", "foo"));
assert!(simple_glob_match("f*", "f"));
assert!(simple_glob_match("*o", "foo"));
assert!(simple_glob_match("*", ""));
assert!(simple_glob_match("?oo", "foo"));
assert!(!simple_glob_match("?oo", "fo"));
assert!(simple_glob_match("f*o", "frogspawn-suo"));
}
#[test]
fn test_scanmatchshfunc_matches_pattern() {
let _g = SHFUNCTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(shfunc_with_body("foo", "echo a"));
tab.add(shfunc_with_body("foobar", "echo b"));
tab.add(shfunc_with_body("baz", "echo c"));
}
let mut matched: Vec<String> = Vec::new();
let count = scanmatchshfunc(Some("foo*"), |name, _| matched.push(name.to_string()));
assert_eq!(count, 2);
matched.sort();
assert_eq!(matched, vec!["foo".to_string(), "foobar".to_string()]);
let total = scanshfunc(|_, _| {});
assert_eq!(total, 3);
fresh_shfunctab();
}
#[test]
fn test_getshfuncfile_returns_filename() {
let _g = SHFUNCTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
let mut f = shfunc_with_body("f", "true");
f.filename = Some("/tmp/zshrs-fns/f".to_string());
tab.add(f);
}
assert_eq!(getshfuncfile("f"), Some("/tmp/zshrs-fns/f".to_string()));
assert_eq!(getshfuncfile("nonexistent"), None);
fresh_shfunctab();
}
#[test]
fn test_generic_addhashnode_displaces_old() {
let mut ht: HashMap<String, Alias> = HashMap::new();
addhashnode(&mut ht, "x", createaliasnode("x", "echo a", 0));
let old = addhashnode2(&mut ht, "x", createaliasnode("x", "echo b", 0));
assert!(old.is_some());
assert_eq!(old.unwrap().text, "echo a");
assert_eq!(gethashnode2(&ht, "x").unwrap().text, "echo b");
}
#[test]
fn test_generic_disable_filters_get() {
let mut ht: HashMap<String, Alias> = HashMap::new();
ht.insert("a".to_string(), createaliasnode("a", "1", 0));
assert!(gethashnode(&ht, "a").is_some());
disablehashnode(&mut ht, "a");
assert!(gethashnode(&ht, "a").is_none());
assert!(gethashnode2(&ht, "a").is_some());
enablehashnode(&mut ht, "a");
assert!(gethashnode(&ht, "a").is_some());
}
#[test]
fn test_scanmatchtable_pattern_and_count() {
let mut ht: HashMap<String, Alias> = HashMap::new();
ht.insert("foo".to_string(), createaliasnode("foo", "1", 0));
ht.insert("foobar".to_string(), createaliasnode("foobar", "2", 0));
ht.insert("baz".to_string(), createaliasnode("baz", "3", 0));
let mut hits: Vec<String> = Vec::new();
let count = scanmatchtable(&ht, Some("foo*"), true, 0, 0, |n, _| {
hits.push(n.to_string())
});
assert_eq!(count, 2);
assert_eq!(hits, vec!["foo".to_string(), "foobar".to_string()]);
}
#[test]
fn test_emptyhashtable_clears() {
let mut ht: HashMap<String, Alias> = HashMap::new();
ht.insert("a".to_string(), createaliasnode("a", "1", 0));
ht.insert("b".to_string(), createaliasnode("b", "2", 0));
assert_eq!(ht.len(), 2);
emptyhashtable(&mut ht);
assert_eq!(ht.len(), 0);
}
#[test]
fn test_resizehashtable_reserves_capacity() {
let mut ht: HashMap<String, i32> = HashMap::new();
let initial_cap = ht.capacity();
resizehashtable(&mut ht, 200);
assert!(ht.capacity() >= 200);
assert!(ht.capacity() >= initial_cap);
}
#[test]
fn test_aliastab_singleton_has_defaults() {
let tab = aliastab_lock().read().unwrap();
assert!(tab.get_including_disabled("run-help").is_some());
assert!(tab.get_including_disabled("which-command").is_some());
}
#[test]
fn test_createaliasnode_sets_flags() {
let a = createaliasnode("foo", "echo bar", ALIAS_GLOBAL as u32);
assert_eq!(a.node.nam, "foo");
assert_eq!(a.text, "echo bar");
assert!((a.node.flags & ALIAS_GLOBAL as i32) != 0);
}
#[test]
fn test_printaliasnode_formats() {
let a = createaliasnode("ll", "ls -la", 0);
let out = printaliasnode(&a, print_flags::WHENCE_VERBOSE);
assert!(out.contains("ll is an alias for ls -la"));
let list = printaliasnode(&a, print_flags::LIST);
assert!(list.starts_with("alias "));
assert!(list.contains("ll=ls -la"));
}
#[test]
fn test_printreswdnode_formats() {
let out = printreswdnode("if", print_flags::WHENCE_WORD);
assert_eq!(out, "if: reserved");
let v = printreswdnode("if", print_flags::WHENCE_VERBOSE);
assert_eq!(v, "if is a reserved word");
}
#[test]
fn test_addhistnode_displaces_old() {
emptyhisttable();
assert_eq!(addhistnode("ls -la", 1), None);
let old = addhistnode("ls -la", 5);
assert_eq!(old, Some(1));
emptyhisttable();
}
#[test]
fn test_freecmdnamnode_removes() {
emptycmdnamtable();
{
let mut tab = cmdnamtab_lock().write().unwrap();
tab.add(cmdnam_unhashed("ls", vec!["/bin".to_string()]));
}
assert!(cmdnamtab_lock().read().unwrap().get("ls").is_some());
freecmdnamnode("ls");
assert!(cmdnamtab_lock().read().unwrap().get("ls").is_none());
}
#[test]
fn test_dircache_set_refcounts() {
dircache_set("k", Some("/usr/bin"));
dircache_set("k", Some("/usr/bin"));
let cache_size = dircache_lock().lock().unwrap().len();
assert!(cache_size >= 1);
}
}
pub fn newhashtable(size: i32, name: &str) -> (String, i32) { (name.to_string(), size)
}
pub fn deletehashtable<T>(ht: &mut HashMap<String, T>) { ht.clear();
}
pub fn addhashnode<T>(ht: &mut HashMap<String, T>, nam: &str, value: T) { ht.insert(nam.to_string(), value);
}
pub fn addhashnode2<T>(ht: &mut HashMap<String, T>, nam: &str, nodeptr: T) -> Option<T> { ht.insert(nam.to_string(), nodeptr)
}
pub fn gethashnode<'a, T: HashNodeFlags>( ht: &'a HashMap<String, T>,
nam: &str,
) -> Option<&'a T> {
ht.get(nam).filter(|t| !t.is_disabled())
}
pub fn gethashnode2<'a, T>(ht: &'a HashMap<String, T>, nam: &str) -> Option<&'a T> { ht.get(nam)
}
pub fn removehashnode<T>(ht: &mut HashMap<String, T>, nam: &str) -> Option<T> { ht.remove(nam)
}
pub fn disablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
if let Some(node) = hn.get_mut(flags) {
node.set_disabled(true);
true
} else {
false
}
}
pub fn enablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
if let Some(node) = hn.get_mut(flags) {
node.set_disabled(false);
true
} else {
false
}
}
pub fn hnamcmp(ap: &str, bp: &str) -> std::cmp::Ordering {
ap.cmp(bp)
}
pub fn scanmatchtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
ht: &HashMap<String, T>,
pattern: Option<&str>,
sorted: bool,
flags1: u32,
flags2: u32,
mut func: F,
) -> i32 {
let mut entries: Vec<(&String, &T)> = ht.iter().collect();
if sorted {
entries.sort_by(|a, b| a.0.cmp(b.0));
}
let mut match_count = 0;
for (name, node) in entries {
if let Some(p) = pattern {
if !simple_glob_match(p, name) {
continue;
}
}
let f = node.flags();
if flags1 != 0 && (f & flags1) == 0 {
continue;
}
if flags2 != 0 && (f & flags2) != 0 {
continue;
}
func(name, node);
match_count += 1;
}
match_count
}
pub fn scanhashtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
ht: &HashMap<String, T>,
sorted: bool,
flags1: u32,
flags2: u32,
func: F,
) -> i32 {
scanmatchtable(ht, None, sorted, flags1, flags2, func)
}
pub fn expandhashtable<T>(ht: &mut HashMap<String, T>) {
let want = ht.len() * 2;
ht.reserve(want.saturating_sub(ht.capacity()));
}
pub fn resizehashtable<T>(ht: &mut HashMap<String, T>, newsize: i32) {
let need = newsize.max(0) as usize;
if need > ht.capacity() {
ht.reserve(need - ht.capacity());
}
}
pub fn emptyhashtable<T>(ht: &mut HashMap<String, T>) { ht.clear();
}
pub fn printhashtabinfo<T>(name: &str, ht: &HashMap<String, T>) -> String { format!(
"name of table : {}\nsize of nodes[] : {}\nnumber of nodes : {}",
name,
ht.capacity(),
ht.len()
)
}
pub fn bin_hashinfo() -> i32 {
let banner = "----------------------------------------------------";
println!("{}", banner);
{
let tab = cmdnamtab_lock().read().expect("cmdnamtab poisoned");
println!("name of table : cmdnamtab");
println!("number of nodes : {}", tab.len());
}
println!("{}", banner);
{
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
println!("name of table : shfunctab");
println!("number of nodes : {}", tab.len());
}
println!("{}", banner);
{
let tab = aliastab_lock().read().expect("aliastab poisoned");
println!("name of table : aliastab");
println!("number of nodes : {}", tab.len());
}
println!("{}", banner);
0
}
pub trait HashNodeFlags {
fn flags(&self) -> u32;
fn set_disabled(&mut self, disabled: bool);
fn is_disabled(&self) -> bool {
self.flags() & (DISABLED as u32) != 0
}
}
impl HashNodeFlags for Alias {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
impl HashNodeFlags for ShFunc {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
impl HashNodeFlags for CmdName {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
impl HashNodeFlags for Reswd {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
pub fn cmdnamtab_lock() -> &'static std::sync::RwLock<cmdnam_table> { static CMDNAMTAB: std::sync::OnceLock<std::sync::RwLock<cmdnam_table>> =
std::sync::OnceLock::new();
CMDNAMTAB.get_or_init(|| std::sync::RwLock::new(cmdnam_table::new()))
}
pub fn aliastab_lock() -> &'static std::sync::RwLock<alias_table> { static ALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
std::sync::OnceLock::new();
ALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::with_defaults()))
}
pub fn sufaliastab_lock() -> &'static std::sync::RwLock<alias_table> {
static SUFALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
std::sync::OnceLock::new();
SUFALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::new()))
}
pub fn reswdtab_lock() -> &'static std::sync::RwLock<reswd_table> { static RESWDTAB: std::sync::OnceLock<std::sync::RwLock<reswd_table>> =
std::sync::OnceLock::new();
RESWDTAB.get_or_init(|| std::sync::RwLock::new(reswd_table::new()))
}
pub fn histtab_lock() -> &'static std::sync::RwLock<HashMap<String, i32>> {
static HISTTAB: std::sync::OnceLock<std::sync::RwLock<HashMap<String, i32>>> =
std::sync::OnceLock::new();
HISTTAB.get_or_init(|| std::sync::RwLock::new(HashMap::new()))
}
pub fn createcmdnamtable() {
let _ = cmdnamtab_lock();
}
pub fn emptycmdnamtable() {
cmdnamtab_lock().write().expect("cmdnamtab poisoned").clear();
}
pub fn hashdir(dir: &str, dir_index: usize) {
cmdnamtab_lock()
.write()
.expect("cmdnamtab poisoned")
.hash_dir(dir, dir_index);
}
pub fn fillcmdnamtable(path: &[String]) {
let mut tab = cmdnamtab_lock().write().expect("cmdnamtab poisoned");
for (idx, dir) in path.iter().enumerate() {
tab.hash_dir(dir, idx);
}
}
pub fn freecmdnamnode(hn: &str) {
cmdnamtab_lock()
.write()
.expect("cmdnamtab poisoned")
.remove(hn);
}
pub fn shfunctab_lock() -> &'static std::sync::RwLock<shfunc_table> { static SHFUNCTAB: std::sync::OnceLock<std::sync::RwLock<shfunc_table>> =
std::sync::OnceLock::new();
SHFUNCTAB.get_or_init(|| std::sync::RwLock::new(shfunc_table::new()))
}
pub fn createshfunctable() {
let _ = shfunctab_lock();
}
pub fn removeshfuncnode(nam: &str) -> Option<ShFunc> {
if let Some(sig_part) = nam.strip_prefix("TRAP") {
if let Some(sig) = crate::ported::signals::getsigidx(sig_part) {
crate::ported::signals::removetrap(sig);
}
}
shfunctab_lock()
.write()
.expect("shfunctab poisoned")
.remove(nam)
}
pub fn disableshfuncnode(hn: &str) {
{
let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
tab.disable(hn);
}
if let Some(sig_part) = hn.strip_prefix("TRAP") {
if let Some(sig) = crate::ported::signals::getsigidx(sig_part) {
crate::ported::signals::unsettrap(sig);
}
}
}
pub fn enableshfuncnode(hn: &str) {
{
let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
tab.enable(hn);
}
if let Some(sig_part) = hn.strip_prefix("TRAP") {
if let Some(sig) = crate::ported::signals::getsigidx(sig_part) {
let _ = crate::ported::signals::settrap(
sig,
None,
crate::ported::zsh_h::ZSIG_FUNC,
);
}
}
}
pub fn freeshfuncnode(hn: &str) {
shfunctab_lock()
.write()
.expect("shfunctab poisoned")
.remove(hn);
}
pub fn scanmatchshfunc<F>(pattern: Option<&str>, mut func: F) -> i32
where
F: FnMut(&str, &ShFunc),
{
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
let mut count = 0;
for (name, entry) in tab.iter() {
let matches = match pattern {
None => true,
Some(p) => simple_glob_match(p, name),
};
if matches {
func(name, entry);
count += 1;
}
}
count
}
pub fn scanshfunc<F>(func: F) -> i32
where
F: FnMut(&str, &ShFunc),
{
scanmatchshfunc(None, func)
}
pub fn printshfuncexpand(nam: &str, _flags: i32) -> Option<String> {
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
let func = tab.get_including_disabled(nam)?;
let body = func.body.clone().unwrap_or_default();
Some(format!(
"{} () {{\n\t{}\n}}",
nam,
body.replace('\t', " ")
))
}
pub fn getshfuncfile(shf: &str) -> Option<String> {
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
tab.get_including_disabled(shf)
.and_then(|f| f.filename.clone())
}
fn simple_glob_match(pattern: &str, name: &str) -> bool {
let pat_bytes = pattern.as_bytes();
let name_bytes = name.as_bytes();
glob_match_inner(pat_bytes, name_bytes)
}
fn glob_match_inner(pat: &[u8], name: &[u8]) -> bool {
if pat.is_empty() {
return name.is_empty();
}
match pat[0] {
b'*' => {
for i in 0..=name.len() {
if glob_match_inner(&pat[1..], &name[i..]) {
return true;
}
}
false
}
b'?' => !name.is_empty() && glob_match_inner(&pat[1..], &name[1..]),
c => !name.is_empty() && name[0] == c && glob_match_inner(&pat[1..], &name[1..]),
}
}
pub fn createreswdtable() {
let _ = reswdtab_lock();
}
pub fn printreswdnode(hn: &str, printflags: u32) -> String {
if printflags & print_flags::WHENCE_WORD != 0 {
format!("{}: reserved", hn)
} else if printflags & print_flags::WHENCE_CSH != 0 {
format!("{}: shell reserved word", hn)
} else if printflags & print_flags::WHENCE_VERBOSE != 0 {
format!("{} is a reserved word", hn)
} else {
hn.to_string()
}
}
#[allow(unused_variables)]
pub fn createaliastable(ht: *mut crate::ported::zsh_h::hashtable) { }
pub fn createaliastables() {
let mut tab = aliastab_lock().write().expect("aliastab poisoned");
tab.add(createaliasnode("run-help", "man", 0)); tab.add(createaliasnode("which-command", "whence", 0)); drop(tab);
let _ = sufaliastab_lock();
}
pub fn freealiasnode(hn: &str) {
let mut tab = aliastab_lock().write().expect("aliastab poisoned");
tab.remove(hn);
}
pub fn printaliasnode(hn: &Alias, printflags: u32) -> String {
let nam = &hn.node.nam;
let af = hn.node.flags;
let is_suffix = (af & ALIAS_SUFFIX as i32) != 0;
let is_global = (af & ALIAS_GLOBAL as i32) != 0;
if printflags & print_flags::NAMEONLY != 0 {
return nam.clone();
}
if printflags & print_flags::WHENCE_WORD != 0 {
let kind = if is_suffix { "suffix alias" }
else if is_global { "global alias" }
else { "alias" };
return format!("{}: {}", nam, kind);
}
if printflags & print_flags::WHENCE_SIMPLE != 0 {
return hn.text.clone();
}
if printflags & print_flags::WHENCE_CSH != 0 {
let qual = if is_suffix { "suffix " }
else if is_global { "globally " }
else { "" };
return format!("{}: {}aliased to {}", nam, qual, hn.text);
}
if printflags & print_flags::WHENCE_VERBOSE != 0 {
let qual = if is_suffix { "hn suffix" }
else if is_global { "hn global" }
else { "an" };
return format!("{} is {} alias for {}", nam, qual, hn.text);
}
if printflags & print_flags::LIST != 0 {
let mut out = String::from("alias ");
if is_suffix { out.push_str("-s "); }
else if is_global { out.push_str("-g "); }
if nam.starts_with('-') || nam.starts_with('+') {
out.push_str("-- ");
}
out.push_str(&format!("{}={}", nam, hn.text));
return out;
}
format!("{}={}", nam, hn.text)
}
pub fn createhisttable() {
let _ = histtab_lock();
}
pub fn emptyhisttable() {
histtab_lock().write().expect("histtab poisoned").clear();
}
pub fn addhistnode(nam: &str, event_id: i32) -> Option<i32> {
histtab_lock()
.write()
.expect("histtab poisoned")
.insert(nam.to_string(), event_id)
}
pub fn freehistnode(nodeptr: &str) {
histtab_lock()
.write()
.expect("histtab poisoned")
.remove(nodeptr);
}
pub fn freehistdata(_unlink: i32) {}
pub fn dircache_set(name: &str, value: Option<&str>) {
let mut cache = dircache_lock().lock().expect("dircache poisoned");
match value {
None => {
if let Some(idx) = cache.iter().position(|e| e.name == name) {
cache[idx].refs -= 1;
if cache[idx].refs <= 0 {
cache.remove(idx);
}
}
}
Some(v) => {
if let Some(idx) = cache.iter().position(|e| e.name == v) {
cache[idx].refs += 1;
} else {
cache.push(dircache_entry { name: v.to_string(), refs: 1 });
}
let _ = name; }
}
}