use crate::history::HistoryEngine;
use crate::options::ZSH_OPTIONS_SET;
use compsys::cache::CompsysCache;
use compsys::CompInitResult;
use parking_lot::Mutex;
use std::collections::HashSet;
use crate::ported::utils::{errflag, ERRFLAG_ERROR};
use std::sync::atomic::Ordering;
use crate::ported::parse::ECBUF;
use crate::ported::zsh_h::{wc_code, wc_data, WC_END, WC_LIST};
use crate::ported::zsh_h::WC_SUBLIST;
use crate::ported::zsh_h::WC_PIPE;
use crate::ported::zsh_h::{WC_ARITH, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT, WC_SELECT, WC_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE, };
use crate::ported::zsh_h::{WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE};
use crate::ported::zsh_h::{WC_CASE_SKIP, WC_CASE_TYPE};
use crate::ported::builtin::RETFLAG;
use crate::ported::zsh_h::{WC_IF_SKIP, WC_IF_TYPE};
use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS};
use crate::ported::zsh_h::{WC_WHILE_SKIP, WC_WHILE_TYPE};
use crate::ported::math::mathevali;
use crate::ported::parse::ecgetstr_wordcode;
use crate::ported::subst::singsub;
use crate::ported::zsh_h::WC_REPEAT_SKIP;
use crate::ported::parse::ecgetstr_wordcode as ecgetstr;
use std::fs;
use std::os::unix::io::FromRawFd;
use std::io::Read;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use crate::ported::zsh_h::{options, MAX_OPS};
use crate::ported::zsh_h::{PM_INTEGER, PM_EFLOAT, PM_FFLOAT, PM_ARRAY, PM_HASHED, PM_LOWER, PM_UPPER, PM_READONLY, PM_EXPORTED, PM_LEFT, PM_RIGHT_B, PM_RIGHT_Z};
use std::ffi::CStr;
use crate::ported::zle::zle_thingy::{listwidgets, getwidgettarget};
use std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::PermissionsExt;
use std::sync::atomic::AtomicI32;
use crate::ported::modules::parameter::*;
use crate::ported::zsh_h::PM_UNDEFINED;
#[allow(unused_imports)]
#[allow(unused_imports)]
pub(crate) use crate::ported::glob::{expand_glob_alternation, find_top_level_tilde};
#[allow(unused_imports)]
pub use crate::ported::math::convbase as format_int_in_base;
pub use crate::ported::params::convbase_underscore;
#[allow(unused_imports)]
pub(crate) use crate::ported::math::{parse_assign, parse_compound, parse_pre_inc};
#[allow(unused_imports)]
pub(crate) use crate::ported::params::getarrvalue;
#[allow(unused_imports)]
#[allow(unused_imports)]
#[allow(unused_imports)]
pub(crate) use crate::func_body_fmt::FuncBodyFmt;
#[allow(unused_imports)]
pub(crate) use crate::ported::utils::base64_decode;
#[allow(unused_imports)]
pub(crate) use crate::ported::utils::{
bufferwords as bufferwords_z, ispwd, printprompt4, quotedzputs,
};
pub(crate) use crate::intercepts::intercept_matches;
pub use crate::intercepts::{AdviceKind, Intercept};
pub use crate::compinit_bg::CompInitBgResult;
use std::io::Write;
use std::sync::LazyLock;
pub(crate) use crate::plugin_cache::PluginSnapshot;
pub(crate) static REGEX_CACHE: LazyLock<Mutex<std::collections::HashMap<String, regex::Regex>>> =
LazyLock::new(|| Mutex::new(std::collections::HashMap::with_capacity(64)));
pub static TRAP_STATE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub use crate::fusevm_bridge::*;
pub(crate) use crate::fusevm_bridge::{ExecutorContext};
pub mod zsh_version {
include!(concat!(env!("OUT_DIR"), "/zsh_version.rs"));
}
pub fn getoutput(cmd: &str) -> String { with_executor(|exec| exec.run_command_substitution(cmd))
}
pub(crate) fn cached_regex(pattern: &str) -> Option<regex::Regex> {
let mut cache = REGEX_CACHE.lock();
if let Some(re) = cache.get(pattern) {
return Some(re.clone());
}
match regex::Regex::new(pattern) {
Ok(re) => {
cache.insert(pattern.to_string(), re.clone());
Some(re)
}
Err(_) => None,
}
}
pub(crate) static BUILTIN_NAMES: LazyLock<HashSet<String>> = LazyLock::new(|| {
let mut s: HashSet<String> = HashSet::new();
for b in crate::ported::builtin::BUILTINS.iter() {
s.insert(b.node.nam.clone());
}
for &n in crate::daemon::builtins::ZSHRS_BUILTIN_NAMES.iter() {
s.insert(n.to_string());
}
s
});
pub(crate) fn slice_array_zero_based(arr: &[String], offset: i64, length: i64) -> Vec<String> {
let n = arr.len() as i64;
if n == 0 {
return Vec::new();
}
let start = if offset < 0 {
(n + offset).max(0) as usize
} else {
(offset as usize).min(arr.len())
};
let take = if length < 0 {
arr.len().saturating_sub(start)
} else {
(length as usize).min(arr.len().saturating_sub(start))
};
arr.iter().skip(start).take(take).cloned().collect()
}
pub(crate) fn slice_positionals(exec: &ShellExecutor, offset: i64, length: i64) -> Vec<String> {
let pp = exec.pparams();
let mut all: Vec<String> = Vec::with_capacity(pp.len() + 1);
all.push(
exec.scalar("0")
.unwrap_or_else(|| std::env::args().next().unwrap_or_default()),
);
for p in pp {
all.push(p);
}
slice_array_zero_based(&all, offset, length)
}
use crate::exec_jobs::{JobState, JobTable};
use crate::parse::{Redirect, RedirectOp, ShellCommand, ShellWord, VarModifier, ZshParamFlag};
use crate::zwc::ZwcFile;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::env;
use std::fs::{File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
pub use crate::bash_complete::{CompSpec, CompMatch, CompGroup, CompState};
pub use crate::ported::modules::zutil::zstyle_entry;
pub use crate::ported::builtin::AutoloadFlags;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopSignal {
Break,
Continue,
}
pub struct SubshellSnapshot {
pub paramtab: HashMap<String, crate::ported::zsh_h::Param>,
pub paramtab_hashed_storage: HashMap<String, indexmap::IndexMap<String, String>>,
pub positional_params: Vec<String>,
pub env_vars: HashMap<String, String>,
pub cwd: Option<std::path::PathBuf>,
pub umask: u32,
pub traps: HashMap<String, String>,
}
#[allow(unused_imports)]
pub(crate) use crate::ported::pattern::{
extract_numeric_ranges, numeric_range_contains, numeric_ranges_to_star,
};
pub struct ShellExecutor {
pub scriptname: Option<String>,
pub scriptfilename: Option<String>,
pub loop_signal: Option<LoopSignal>,
pub subshell_snapshots: Vec<SubshellSnapshot>,
pub inline_env_stack: Vec<Vec<(String, Option<String>, Option<String>)>>,
pub current_command_glob_failed: std::cell::Cell<bool>,
pub jobs: JobTable,
pub fpath: Vec<PathBuf>,
pub zwc_cache: HashMap<PathBuf, ZwcFile>,
pub history: Option<HistoryEngine>,
pub session_histnum: i64,
pub(crate) process_sub_counter: u32,
pub traps: HashMap<String, String>,
pub completions: HashMap<String, CompSpec>, pub comp_matches: Vec<CompMatch>, pub comp_groups: Vec<CompGroup>, pub comp_state: CompState, pub zstyles: Vec<zstyle_entry>, pub comp_words: Vec<String>, pub comp_current: i32, pub comp_prefix: String, pub comp_suffix: String, pub comp_iprefix: String, pub comp_isuffix: String, pub local_scope_depth: usize,
pub pending_underscore: Option<String>,
pub in_dq_context: u32,
pub in_scalar_assign: u32,
pub session_history_ids: Vec<i64>,
pub open_fds: HashMap<i32, std::fs::File>,
pub next_fd: i32,
pub profiling_enabled: bool,
pub compsys_cache: Option<CompsysCache>,
pub compinit_pending: Option<(
std::sync::mpsc::Receiver<CompInitBgResult>,
std::time::Instant,
)>,
pub plugin_cache: Option<crate::plugin_cache::PluginCache>,
pub deferred_compdefs: Vec<Vec<String>>,
pub returning: Option<i32>, pub breaking: i32, pub continuing: i32, pub zsh_compat: bool,
pub bash_compat: bool,
pub posix_mode: bool,
pub worker_pool: std::sync::Arc<crate::worker::WorkerPool>,
pub intercepts: Vec<Intercept>,
pub async_jobs: HashMap<u32, crossbeam_channel::Receiver<(i32, String)>>,
pub next_async_id: u32,
pub defer_stack: Vec<Vec<String>>,
pub redirect_scope_stack: Vec<Vec<(i32, i32)>>,
pub redirect_failed: bool,
pub pending_stdin: Option<String>,
pub functions_compiled: HashMap<String, fusevm::Chunk>,
pub function_source: HashMap<String, String>,
pub function_line_base: HashMap<String, i64>,
pub function_def_file: HashMap<String, Option<String>>,
pub prompt_funcstack: Vec<(String, i64, Option<String>)>,
pub tied_scalar_to_array: HashMap<String, (String, String)>,
pub tied_array_to_scalar: HashMap<String, (String, String)>,
pub buffer_stack: Vec<String>,
}
impl ShellExecutor {
pub fn set_scalar(&mut self, name: String, value: String) {
crate::ported::params::setsparam(&name, &value); }
pub fn pparams(&self) -> Vec<String> {
crate::ported::builtin::PPARAMS
.lock()
.map(|p| p.clone())
.unwrap_or_default()
}
pub fn set_pparams(&mut self, params: Vec<String>) {
if let Ok(mut p) = crate::ported::builtin::PPARAMS.lock() {
*p = params;
}
}
pub fn param_flags(&self, name: &str) -> i32 {
crate::ported::params::paramtab().read()
.ok()
.and_then(|t| t.get(name).map(|p| p.node.flags))
.unwrap_or(0)
}
pub fn is_integer_param(&self, name: &str) -> bool {
(self.param_flags(name) as u32 & crate::ported::zsh_h::PM_INTEGER) != 0
}
pub fn is_float_param(&self, name: &str) -> bool {
let f = self.param_flags(name) as u32;
(f & (crate::ported::zsh_h::PM_EFLOAT | crate::ported::zsh_h::PM_FFLOAT)) != 0
}
pub fn is_lowercase_param(&self, name: &str) -> bool {
(self.param_flags(name) as u32 & crate::ported::zsh_h::PM_LOWER) != 0
}
pub fn is_uppercase_param(&self, name: &str) -> bool {
(self.param_flags(name) as u32 & crate::ported::zsh_h::PM_UPPER) != 0
}
pub fn is_readonly_param(&self, name: &str) -> bool {
(self.param_flags(name) as u32 & crate::ported::zsh_h::PM_READONLY) != 0
}
pub fn last_status(&self) -> i32 {
crate::ported::builtin::LASTVAL
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_last_status(&mut self, status: i32) {
crate::ported::builtin::LASTVAL
.store(status, std::sync::atomic::Ordering::Relaxed);
}
pub fn set_array(&mut self, name: String, value: Vec<String>) {
crate::ported::params::setaparam(&name, value); }
pub fn set_assoc(&mut self, name: String, value: indexmap::IndexMap<String, String>) {
let mut flat: Vec<String> = Vec::with_capacity(value.len() * 2);
for (k, v) in &value {
flat.push(k.clone());
flat.push(v.clone());
}
crate::ported::params::sethparam(&name, flat); }
pub fn scalar(&self, name: &str) -> Option<String> {
crate::ported::params::getsparam(name)
}
pub fn array(&self, name: &str) -> Option<Vec<String>> {
crate::ported::params::paramtab().read()
.ok()
.and_then(|t| t.get(name).and_then(|pm| pm.u_arr.clone()))
}
pub fn assoc(&self, name: &str) -> Option<indexmap::IndexMap<String, String>> {
crate::ported::params::paramtab_hashed_storage()
.lock().ok()
.and_then(|m| m.get(name).cloned())
}
pub fn has_scalar(&self, name: &str) -> bool {
crate::ported::params::getsparam(name).is_some()
}
pub fn has_array(&self, name: &str) -> bool {
crate::ported::params::paramtab().read()
.ok()
.and_then(|t| t.get(name).map(|pm| pm.u_arr.is_some()))
.unwrap_or(false)
}
pub fn has_assoc(&self, name: &str) -> bool {
crate::ported::params::paramtab_hashed_storage()
.lock()
.ok()
.map(|m| m.contains_key(name))
.unwrap_or(false)
}
pub fn unset_assoc(&mut self, name: &str) {
if let Some(tab) = crate::ported::params::paramtab().write().ok().as_deref_mut() {
tab.remove(name);
}
let _ = crate::ported::params::paramtab_hashed_storage()
.lock().ok().as_deref_mut()
.map(|m| m.remove(name));
}
pub fn alias(&self, name: &str) -> Option<String> {
let tab = crate::ported::hashtable::aliastab_lock().read().ok()?;
let a = tab.get(name)?;
if (a.node.flags & crate::ported::zsh_h::ALIAS_GLOBAL as i32) != 0 {
None
} else {
Some(a.text.clone())
}
}
pub fn global_alias(&self, name: &str) -> Option<String> {
let tab = crate::ported::hashtable::aliastab_lock().read().ok()?;
let a = tab.get(name)?;
if (a.node.flags & crate::ported::zsh_h::ALIAS_GLOBAL as i32) != 0 {
Some(a.text.clone())
} else {
None
}
}
pub fn suffix_alias(&self, name: &str) -> Option<String> {
let tab = crate::ported::hashtable::sufaliastab_lock().read().ok()?;
Some(tab.get(name)?.text.clone())
}
pub fn set_alias(&mut self, name: String, value: String) {
if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
tab.add(crate::ported::hashtable::createaliasnode(&name, &value, 0));
}
}
pub fn set_global_alias(&mut self, name: String, value: String) {
if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
tab.add(crate::ported::hashtable::createaliasnode(
&name, &value, crate::ported::zsh_h::ALIAS_GLOBAL as u32,
));
}
}
pub fn set_suffix_alias(&mut self, name: String, value: String) {
if let Ok(mut tab) = crate::ported::hashtable::sufaliastab_lock().write() {
tab.add(crate::ported::hashtable::createaliasnode(&name, &value, 0));
}
}
pub fn unset_alias(&mut self, name: &str) {
if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
tab.remove(name);
}
}
pub fn unset_suffix_alias(&mut self, name: &str) {
if let Ok(mut tab) = crate::ported::hashtable::sufaliastab_lock().write() {
tab.remove(name);
}
}
pub fn alias_entries(&self) -> Vec<(String, String)> {
if let Ok(tab) = crate::ported::hashtable::aliastab_lock().read() {
tab.iter_sorted()
.into_iter()
.filter(|(_, a)| (a.node.flags
& crate::ported::zsh_h::ALIAS_GLOBAL as i32) == 0)
.map(|(k, a)| (k.clone(), a.text.clone()))
.collect()
} else {
Vec::new()
}
}
pub fn global_alias_entries(&self) -> Vec<(String, String)> {
if let Ok(tab) = crate::ported::hashtable::aliastab_lock().read() {
tab.iter_sorted()
.into_iter()
.filter(|(_, a)| (a.node.flags
& crate::ported::zsh_h::ALIAS_GLOBAL as i32) != 0)
.map(|(k, a)| (k.clone(), a.text.clone()))
.collect()
} else {
Vec::new()
}
}
pub fn suffix_alias_entries(&self) -> Vec<(String, String)> {
if let Ok(tab) = crate::ported::hashtable::sufaliastab_lock().read() {
tab.iter_sorted()
.into_iter()
.map(|(k, a)| (k.clone(), a.text.clone()))
.collect()
} else {
Vec::new()
}
}
pub fn unset_array(&mut self, name: &str) {
if let Some(tab) = crate::ported::params::paramtab().write().ok().as_deref_mut() {
tab.remove(name);
}
}
pub fn unset_scalar(&mut self, name: &str) {
if let Some(tab) = crate::ported::params::paramtab().write().ok().as_deref_mut() {
tab.remove(name);
}
}
pub(crate) fn unset_var(&mut self, name: &str) {
if let Some(tab) = crate::ported::params::paramtab().write().ok().as_deref_mut() {
tab.remove(name); }
let _ = crate::ported::params::paramtab_hashed_storage()
.lock().ok().as_deref_mut()
.map(|m| m.remove(name));
}
pub fn new() -> Self {
tracing::debug!("ShellExecutor::new() initializing");
if let Ok(pwd_env) = env::var("PWD") {
let valid = ispwd(&pwd_env);
if !valid {
if let Ok(real) = env::current_dir() {
env::set_var("PWD", &real);
}
}
} else if let Ok(real) = env::current_dir() {
env::set_var("PWD", &real);
}
let fpath = env::var("FPATH")
.unwrap_or_default()
.split(':')
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect();
let history = HistoryEngine::new().ok();
let mut variables = HashMap::new();
variables.insert("ZSH_VERSION".to_string(), zsh_version::ZSH_VERSION.to_string()); variables.insert("ZSH_PATCHLEVEL".to_string(),
zsh_version::ZSH_PATCHLEVEL.to_string()); variables.insert("ZSH_NAME".to_string(), "zsh".to_string());
variables.insert(
"ZSH_ARGZERO".to_string(),
std::env::args().next().unwrap_or_else(|| "zsh".to_string()),
);
variables.insert(
"WORDCHARS".to_string(),
"*?_-.[]~=/&;!#$%^(){}<>".to_string(),
);
variables.insert(
"SHLVL".to_string(),
env::var("SHLVL")
.map(|v| {
v.parse::<i32>()
.map(|n| (n + 1).to_string())
.unwrap_or_else(|_| "1".to_string())
})
.unwrap_or_else(|_| "1".to_string()),
);
variables.insert("IFS".to_string(), " \t\n\0".to_string());
variables.insert("OPTIND".to_string(), "1".to_string());
variables.insert("OPTERR".to_string(), "1".to_string());
variables.insert("_".to_string(), String::new());
variables.insert("histchars".to_string(),
crate::ported::params::histcharsgetfn());
variables.insert("MAILCHECK".to_string(), "60".to_string()); variables.insert("KEYTIMEOUT".to_string(), "40".to_string()); variables.insert("LISTMAX".to_string(), "100".to_string()); variables.insert("WATCHFMT".to_string(),
crate::ported::modules::watch::DEFAULT_WATCHFMT.to_string());
variables.insert("FUNCNEST".to_string(), "100".to_string());
unsafe {
libc::setlocale(libc::LC_ALL, c"".as_ptr());
}
crate::ported::hashtable::createaliastables();
let mut arrays: HashMap<String, Vec<String>> = HashMap::new();
let path_dirs: Vec<String> = env::var("PATH")
.unwrap_or_default()
.split(':')
.map(|s| s.to_string())
.collect();
arrays.insert("path".to_string(), path_dirs);
if crate::ported::options::opt_state_len() == 0 {
for (k, v) in Self::default_options() {
crate::ported::options::opt_state_set(&k, v);
}
}
let mut exec = Self {
scriptname: None,
scriptfilename: None,
loop_signal: None,
subshell_snapshots: Vec::new(),
inline_env_stack: Vec::new(),
current_command_glob_failed: std::cell::Cell::new(false),
jobs: JobTable::new(),
fpath,
zwc_cache: HashMap::new(),
history,
session_histnum: 0,
completions: HashMap::new(),
process_sub_counter: 0,
traps: HashMap::new(),
comp_matches: Vec::new(),
comp_groups: Vec::new(),
comp_state: CompState::default(),
zstyles: Vec::new(),
comp_words: Vec::new(),
comp_current: 0,
comp_prefix: String::new(),
comp_suffix: String::new(),
comp_iprefix: String::new(),
comp_isuffix: String::new(),
local_scope_depth: 0,
pending_underscore: None,
in_dq_context: 0,
in_scalar_assign: 0,
session_history_ids: Vec::new(),
open_fds: HashMap::new(),
next_fd: 10,
profiling_enabled: false,
compsys_cache: {
let cache_path = compsys::cache::default_cache_path();
if cache_path.exists() {
let db_size = std::fs::metadata(&cache_path).map(|m| m.len()).unwrap_or(0);
match CompsysCache::open(&cache_path) {
Ok(c) => {
tracing::info!(
db_bytes = db_size,
path = %cache_path.display(),
"compsys: sqlite cache opened"
);
Some(c)
}
Err(e) => {
tracing::warn!(error = %e, "compsys: failed to open cache");
None
}
}
} else {
tracing::debug!("compsys: no cache at {}", cache_path.display());
None
}
},
compinit_pending: None, plugin_cache: {
let pc_path = crate::plugin_cache::default_cache_path();
if let Some(parent) = pc_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
match crate::plugin_cache::PluginCache::open(&pc_path) {
Ok(pc) => {
let (plugins, functions) = pc.stats();
tracing::info!(
plugins,
cached_functions = functions,
path = %pc_path.display(),
"plugin_cache: sqlite opened"
);
Some(pc)
}
Err(e) => {
tracing::warn!(error = %e, "plugin_cache: failed to open");
None
}
}
},
deferred_compdefs: Vec::new(),
returning: None,
breaking: 0,
continuing: 0,
zsh_compat: false,
bash_compat: false,
posix_mode: false,
worker_pool: {
let config = crate::config::load();
let pool_size = crate::config::resolve_pool_size(&config.worker_pool);
std::sync::Arc::new(crate::worker::WorkerPool::new(pool_size))
},
intercepts: Vec::new(),
async_jobs: HashMap::new(),
next_async_id: 1,
defer_stack: Vec::new(),
redirect_scope_stack: Vec::new(),
redirect_failed: false,
pending_stdin: None,
functions_compiled: HashMap::new(),
function_source: HashMap::new(),
function_line_base: HashMap::new(),
function_def_file: HashMap::new(),
prompt_funcstack: Vec::new(),
tied_scalar_to_array: HashMap::new(),
tied_array_to_scalar: HashMap::new(),
buffer_stack: Vec::new(),
};
let fpath_arr: Vec<String> = exec
.fpath
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
if !fpath_arr.is_empty() {
exec.set_array("fpath".to_string(), fpath_arr);
}
if let Ok(path) = env::var("PATH") {
let path_arr: Vec<String> = path
.split(':')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if !path_arr.is_empty() {
exec.set_array("path".to_string(), path_arr);
}
}
for (scalar, arr) in [
("PATH", "path"),
("FPATH", "fpath"),
("MANPATH", "manpath"),
("CDPATH", "cdpath"),
("MODULE_PATH", "module_path"),
] {
exec.tied_array_to_scalar
.insert(arr.to_string(), (scalar.to_string(), ":".to_string()));
exec.tied_scalar_to_array
.insert(scalar.to_string(), (arr.to_string(), ":".to_string()));
}
for (k, v) in &variables {
crate::ported::params::setsparam(k, v); }
for (k, v) in &arrays {
crate::ported::params::setaparam(k, v.clone()); }
exec
}
pub fn add_fpath(&mut self, path: PathBuf) {
if !self.fpath.contains(&path) {
self.fpath.insert(0, path);
}
}
pub fn execute_script_file(&mut self, file_path: &str) -> Result<i32, String> {
let path = Path::new(file_path);
let abs_path = path
.canonicalize()
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.to_string();
if let Some(bc_blob) = crate::script_cache::try_load_bytes(path) {
if let Ok(chunk) = bincode::deserialize::<fusevm::Chunk>(&bc_blob) {
if !chunk.ops.is_empty() {
tracing::trace!(
path = %abs_path,
ops = chunk.ops.len(),
"execute_script_file: bytecode cache hit"
);
let mut vm = fusevm::VM::new(chunk);
register_builtins(&mut vm);
let _ctx = ExecutorContext::enter(self);
match vm.run() {
fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
self.set_last_status(vm.last_status);
}
fusevm::VMResult::Error(e) => {
return Err(format!("VM error: {}", e));
}
}
return Ok(self.last_status());
}
}
}
let content =
std::fs::read_to_string(file_path).map_err(|e| format!("{}: {}", file_path, e))?;
let saved_errflag = errflag.load(Ordering::Relaxed);
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
crate::ported::parse::parse_init(&content);
let program = crate::ported::parse::parse();
let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
errflag.store(saved_errflag, Ordering::Relaxed);
if parse_failed {
return Err("parse error".to_string());
}
let compiler = crate::compile_zsh::ZshCompiler::new();
let chunk = compiler.compile(&program);
if let Ok(blob) = bincode::serialize(&chunk) {
let _ = crate::script_cache::try_save_bytes(path, &blob);
tracing::trace!(
path = %abs_path,
bytes = blob.len(),
"execute_script_file: bytecode cached"
);
}
if !chunk.ops.is_empty() {
let mut vm = fusevm::VM::new(chunk);
register_builtins(&mut vm);
let _ctx = ExecutorContext::enter(self);
match vm.run() {
fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
self.set_last_status(vm.last_status);
}
fusevm::VMResult::Error(e) => {
return Err(format!("VM error: {}", e));
}
}
}
Ok(self.last_status())
}
pub fn exec_wordcode(&mut self) -> i32 {
let buf = ECBUF.with_borrow(|b| b.clone());
let (status, _next) = self.exec_list_wordcode(&buf, 0);
self.set_last_status(status);
status
}
pub fn exec_list_wordcode(&mut self, buf: &[u32], mut pc: usize) -> (i32, usize) {
let mut last_status: i32 = 0;
while pc < buf.len() {
let code = wc_code(buf[pc]);
if code == WC_END {
pc += 1;
break;
}
if code != WC_LIST {
pc += 1;
continue;
}
let header = buf[pc];
let skip = (wc_data(header)
>> crate::ported::zsh_h::WC_LIST_FREE) as usize;
pc += 1;
let (s, _) = self.exec_sublist_wordcode(buf, pc);
last_status = s;
pc += skip;
}
(last_status, pc)
}
pub fn exec_sublist_wordcode(&mut self, buf: &[u32], mut pc: usize) -> (i32, usize) {
let mut last_status: i32 = 0;
if pc < buf.len() && wc_code(buf[pc]) == WC_SUBLIST {
let header = buf[pc];
let skip = (wc_data(header) >> 7) as usize;
pc += 1;
let (s, _) = self.exec_pline_wordcode(buf, pc);
last_status = s;
pc += skip;
}
(last_status, pc)
}
pub fn exec_pline_wordcode(&mut self, buf: &[u32], mut pc: usize) -> (i32, usize) {
let mut last_status: i32 = 0;
if pc < buf.len() && wc_code(buf[pc]) == WC_PIPE {
let header = buf[pc];
let skip = ((wc_data(header) >> 1) & 0xffff) as usize;
pc += 1;
let (s, _) = self.exec_cmd_wordcode(buf, pc);
last_status = s;
pc += skip;
}
(last_status, pc)
}
pub fn exec_cmd_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
match wc_code(buf[pc]) {
WC_SIMPLE => self.exec_simple_wordcode(buf, pc),
WC_SUBSH => self.exec_subsh_wordcode(buf, pc),
WC_CURSH => self.exec_cursh_wordcode(buf, pc),
WC_FOR => self.exec_for_wordcode(buf, pc),
WC_SELECT => self.exec_select_wordcode(buf, pc),
WC_CASE => self.exec_case_wordcode(buf, pc),
WC_IF => self.exec_if_wordcode(buf, pc),
WC_WHILE => self.exec_while_wordcode(buf, pc),
WC_REPEAT => self.exec_repeat_wordcode(buf, pc),
WC_FUNCDEF => self.exec_funcdef_wordcode(buf, pc),
WC_TIMED => self.exec_timed_wordcode(buf, pc),
WC_COND => self.exec_cond_wordcode(buf, pc),
WC_ARITH => self.exec_arith_wordcode(buf, pc),
WC_TRY => self.exec_try_wordcode(buf, pc),
_ => (0, pc + 1),
}
}
pub fn exec_for_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
let header = buf[pc];
let _type_bits = WC_FOR_TYPE(header);
let skip = WC_FOR_SKIP(header) as usize;
let _ = WC_FOR_LIST;
let mut last_status: i32 = 0;
let end_pc = pc + 1 + skip;
let body_pc = pc + 2;
if body_pc < end_pc {
let (s, _) = self.exec_list_wordcode(buf, body_pc);
last_status = s;
}
(last_status, end_pc)
}
pub fn exec_select_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.exec_for_wordcode(buf, pc)
}
pub fn exec_case_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
let header = buf[pc];
let _type_bits = WC_CASE_TYPE(header);
let skip = WC_CASE_SKIP(header) as usize;
let mut last_status: i32 = 0;
let end_pc = pc + 1 + skip;
let body_pc = pc + 1;
if body_pc < end_pc {
let (s, _) = self.exec_list_wordcode(buf, body_pc);
last_status = s;
}
(last_status, end_pc)
}
pub fn exec_if_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
let header = buf[pc];
let skip = WC_IF_SKIP(header) as usize;
let end_pc = pc + 1 + skip;
let mut cur = pc + 1;
let mut run: i32 = 0; let mut s = 0; let mut last_status: i32 = 0;
while cur < end_pc {
if cur >= buf.len() {
break;
}
let code = buf[cur];
cur += 1;
if wc_code(code) != WC_IF {
run = 1;
cur -= 1;
break;
}
if WC_IF_TYPE(code) == 2 {
run = 2;
break;
}
let next = cur + WC_IF_SKIP(code) as usize;
let (cond_status, after_cond) = self.exec_list_wordcode(buf, cur);
last_status = cond_status;
if cond_status == 0 {
run = 1;
cur = after_cond;
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
break;
}
s = 1;
cur = next;
}
let _ = s;
if run != 0 && cur < end_pc {
let (body_status, _) = self.exec_list_wordcode(buf, cur);
last_status = body_status;
} else if RETFLAG.load(Ordering::SeqCst) == 0
&& (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
{
last_status = 0;
}
(last_status, end_pc)
}
pub fn exec_while_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
let header = buf[pc];
let isuntil = WC_WHILE_TYPE(header) == 2;
let skip = WC_WHILE_SKIP(header) as usize;
let end_pc = pc + 1 + skip;
let loop_pc = pc + 1;
LOOPS.fetch_add(1, Ordering::SeqCst);
let mut last_status: i32 = 0;
let mut oldval: i32 = 0;
let mut iters = 0u64;
const ITER_CAP: u64 = 1_000_000;
loop {
iters += 1;
if iters > ITER_CAP {
break;
}
let (cond_status, after_cond) = self.exec_list_wordcode(buf, loop_pc);
last_status = cond_status;
let cond_passed = (cond_status == 0) ^ isuntil;
if !cond_passed {
if BREAKS.load(Ordering::SeqCst) > 0 {
BREAKS.fetch_sub(1, Ordering::SeqCst);
}
if RETFLAG.load(Ordering::SeqCst) == 0 {
last_status = oldval;
}
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
if BREAKS.load(Ordering::SeqCst) > 0 {
BREAKS.fetch_sub(1, Ordering::SeqCst);
}
break;
}
let (body_status, _) = self.exec_list_wordcode(buf, after_cond);
last_status = body_status;
if BREAKS.load(Ordering::SeqCst) > 0 {
let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
break;
}
CONTFLAG.store(0, Ordering::SeqCst);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
last_status = 1;
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
break;
}
oldval = last_status;
}
LOOPS.fetch_sub(1, Ordering::SeqCst);
(last_status, end_pc)
}
pub fn exec_repeat_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
let header = buf[pc];
let skip = WC_REPEAT_SKIP(header) as usize;
let end_pc = pc + 1 + skip;
let (count_expr_raw, after_count) = ecgetstr_wordcode(buf, pc + 1);
let count_expr_sub = singsub(&count_expr_raw);
let count_expr = crate::ported::lex::untokenize(&count_expr_sub);
let count_val = mathevali(&count_expr).unwrap_or(0);
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
return (1, end_pc);
}
let mut last_status: i32 = 0; LOOPS.fetch_add(1, Ordering::SeqCst);
let loop_body_pc = after_count;
let mut remaining = count_val;
while remaining > 0 {
remaining -= 1;
let (s, _) = self.exec_list_wordcode(buf, loop_body_pc);
last_status = s;
if BREAKS.load(Ordering::SeqCst) > 0 {
let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
break;
}
CONTFLAG.store(0, Ordering::SeqCst);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
last_status = 1;
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
break;
}
}
LOOPS.fetch_sub(1, Ordering::SeqCst);
(last_status, end_pc)
}
pub fn exec_funcdef_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
pub fn exec_subsh_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
pub fn exec_cursh_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
pub fn exec_timed_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
pub fn exec_cond_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
pub fn exec_arith_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
pub fn exec_try_wordcode(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
self.skip_form(buf, pc)
}
fn skip_form(&mut self, buf: &[u32], pc: usize) -> (i32, usize) {
if pc >= buf.len() {
return (0, pc);
}
let skip = wc_data(buf[pc]) as usize;
(0, pc + 1 + skip)
}
pub fn exec_simple_wordcode(&mut self, buf: &[u32], mut pc: usize) -> (i32, usize) {
let mut last_status: i32 = 0;
if pc < buf.len() && wc_code(buf[pc]) == WC_SIMPLE {
let header = buf[pc];
let nwords = wc_data(header) as usize;
pc += 1;
let mut argv: Vec<String> = Vec::with_capacity(nwords);
for _ in 0..nwords {
let (word, next) = ecgetstr(buf, pc);
argv.push(word);
pc = next;
}
if !argv.is_empty() {
last_status = self.invoke_argv_wordcode(&argv);
}
}
(last_status, pc)
}
fn invoke_argv_wordcode(&mut self, argv: &[String]) -> i32 {
let script = argv
.iter()
.map(|s| {
if s.chars().any(|c| c.is_whitespace() || "\"'`$\\|;&<>(){}[]*?~".contains(c)) {
format!("'{}'", s.replace('\'', "'\\''"))
} else {
s.clone()
}
})
.collect::<Vec<_>>()
.join(" ");
self.execute_script_zsh_pipeline(&script).unwrap_or(1)
}
pub fn execute_script_zsh_pipeline(&mut self, script: &str) -> Result<i32, String> {
let saved_errflag = errflag.load(Ordering::Relaxed);
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
crate::ported::parse::parse_init(script);
let program = crate::ported::parse::parse();
let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
errflag.store(saved_errflag, Ordering::Relaxed);
if parse_failed {
return Err("parse error".to_string());
}
let compiler = crate::compile_zsh::ZshCompiler::new();
let chunk = compiler.compile(&program);
if chunk.ops.is_empty() {
return Ok(self.last_status());
}
let mut vm = fusevm::VM::new(chunk);
register_builtins(&mut vm);
{
let _ctx = ExecutorContext::enter(self);
match vm.run() {
fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
self.set_last_status(vm.last_status);
}
fusevm::VMResult::Error(e) => return Err(format!("VM error: {}", e)),
}
}
if let Some(action) = self.traps.remove("EXIT") {
tracing::debug!("firing EXIT trap (new pipeline)");
let _ = self.execute_script_zsh_pipeline(&action);
}
Ok(self.last_status())
}
#[tracing::instrument(skip(self, script), fields(len = script.len()))]
pub fn execute_script(&mut self, script: &str) -> Result<i32, String> {
self.execute_script_zsh_pipeline(script)
}
pub fn function_exists(&self, name: &str) -> bool {
if self.functions_compiled.contains_key(name) {
return true;
}
crate::ported::hashtable::shfunctab_lock().read().ok()
.map(|t| t.get(name).is_some())
.unwrap_or(false)
}
pub fn function_definition_text(&self, name: &str) -> Option<String> {
self.function_source.get(name).cloned()
}
pub fn remove_function(&mut self, name: &str) -> bool {
let a = self.functions_compiled.remove(name).is_some();
let c = self.function_source.remove(name).is_some();
let _ = self.function_line_base.remove(name);
let _ = self.function_def_file.remove(name);
a || c
}
pub fn function_names(&self) -> Vec<String> {
let mut set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for k in self.functions_compiled.keys() {
set.insert(k.clone());
}
for k in self.function_source.keys() {
set.insert(k.clone());
}
set.into_iter().collect()
}
pub fn dispatch_function_call(&mut self, name: &str, args: &[String]) -> Option<i32> {
let chunk = self.functions_compiled.get(name).cloned()?;
let funcnest_limit: usize = self
.scalar("FUNCNEST")
.and_then(|s| s.parse().ok())
.unwrap_or(100);
if self.local_scope_depth >= funcnest_limit {
eprintln!(
"{}: maximum nested function level reached; increase FUNCNEST?",
name
);
return Some(1);
}
let saved_params = self.pparams();
self.set_pparams(args.to_vec());
let display_name = if name.starts_with("_zshrs_anon_") {
"(anon)".to_string()
} else {
name.to_string()
};
let saved_zero = crate::ported::params::getsparam("0");
self.set_scalar("0".to_string(), display_name);
self.local_scope_depth += 1;
crate::ported::params::locallevel.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let line_base = self
.function_line_base
.get(name)
.copied()
.unwrap_or(0);
let def_file = self.function_def_file.get(name).cloned().flatten();
self.prompt_funcstack
.push((name.to_string(), line_base, def_file));
let mut vm = fusevm::VM::new(chunk);
register_builtins(&mut vm);
let _ctx = ExecutorContext::enter(self);
let _ = vm.run();
let status = vm.last_status;
drop(_ctx);
self.set_pparams(saved_params);
self.prompt_funcstack.pop();
crate::ported::params::endparamscope();
self.local_scope_depth -= 1;
match saved_zero {
Some(v) => {
self.set_scalar("0".to_string(), v);
}
None => {
self.unset_scalar("0");
}
}
if let Some(ret) = self.returning.take() {
self.set_last_status(ret);
Some(ret)
} else {
self.set_last_status(status);
Some(status)
}
}
pub(crate) fn execute_external(
&mut self,
cmd: &str,
args: &[String],
redirects: &[Redirect],
) -> Result<i32, String> {
self.execute_external_bg(cmd, args, redirects, false)
}
fn execute_external_bg(
&mut self,
cmd: &str,
args: &[String],
_redirects: &[Redirect],
background: bool,
) -> Result<i32, String> {
tracing::trace!(cmd, bg = background, "exec external");
let mut command = Command::new(cmd);
command.args(args);
if background {
match command.spawn() {
Ok(child) => {
let pid = child.id();
let cmd_str = format!("{} {}", cmd, args.join(" "));
let job_id = self.jobs.add_job(child, cmd_str, JobState::Running);
println!("[{}] {}", job_id, pid);
Ok(0)
}
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
if cmd.starts_with('/') {
eprintln!("zshrs:1: no such file or directory: {}", cmd);
} else {
eprintln!("zshrs:1: command not found: {}", cmd);
}
Ok(127)
} else {
Err(format!("zshrs: {}: {}", cmd, e))
}
}
}
} else {
match command.status() {
Ok(status) => Ok(status.code().unwrap_or(1)),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
if cmd.starts_with('/') {
eprintln!("zshrs:1: no such file or directory: {}", cmd);
} else {
eprintln!("zshrs:1: command not found: {}", cmd);
}
Ok(127)
} else if e.kind() == io::ErrorKind::PermissionDenied {
eprintln!("zshrs:1: permission denied: {}", cmd);
Ok(126)
} else {
Err(format!("zshrs: {}: {}", cmd, e))
}
}
}
}
}
pub(crate) fn collect_until_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
let mut result = String::new();
let mut depth = 1;
for c in chars.by_ref() {
if c == '(' {
depth += 1;
result.push(c);
} else if c == ')' {
depth -= 1;
if depth == 0 {
break;
}
result.push(c);
} else {
result.push(c);
}
}
result
}
pub(crate) fn collect_until_double_paren(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> String {
let mut result = String::new();
let mut arith_depth = 1; let mut paren_depth = 0;
while let Some(c) = chars.next() {
if c == '(' {
if paren_depth == 0 && chars.peek() == Some(&'(') {
paren_depth += 1;
result.push(c);
} else {
paren_depth += 1;
result.push(c);
}
} else if c == ')' {
if paren_depth > 0 {
paren_depth -= 1;
result.push(c);
} else if chars.peek() == Some(&')') {
chars.next();
arith_depth -= 1;
if arith_depth == 0 {
break;
}
result.push_str("))");
} else {
result.push(c);
}
} else {
result.push(c);
}
}
result
}
fn simple_cmd_words(&mut self, cmd_str: &str) -> Vec<String> {
let saved_errflag = errflag.load(Ordering::Relaxed);
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
crate::ported::parse::parse_init(cmd_str);
let prog = crate::ported::parse::parse();
let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
errflag.store(saved_errflag, Ordering::Relaxed);
if parse_failed {
return Vec::new();
}
let first = match prog.lists.first() {
Some(l) => l,
None => return Vec::new(),
};
let pipe = &first.sublist.pipe;
if let crate::parse::ZshCommand::Simple(simple) = &pipe.cmd {
simple
.words
.iter()
.map(|w| {
let untoked = crate::lex::untokenize(w);
crate::ported::subst::singsub(&untoked)
})
.collect()
} else {
Vec::new()
}
}
pub(crate) fn run_process_sub_in(&mut self, cmd_str: &str) -> String {
let words = self.simple_cmd_words(cmd_str);
let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
let fifo_counter = self.process_sub_counter;
self.process_sub_counter += 1;
let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
let _ = fs::remove_file(&fifo_path);
if nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU).is_err() {
return String::new();
}
let fifo_clone = fifo_path.clone();
if !words.is_empty() {
let cmd_name = words[0].clone();
let args: Vec<String> = words[1..].to_vec();
self.worker_pool.submit(move || {
if let Ok(fifo) = fs::OpenOptions::new().write(true).open(&fifo_clone) {
let _ = Command::new(&cmd_name)
.args(&args)
.stdout(fifo)
.stderr(Stdio::inherit())
.status();
}
let _ = fs::remove_file(&fifo_clone);
});
}
fifo_path
}
pub(crate) fn run_process_sub_out(&mut self, cmd_str: &str) -> String {
let words = self.simple_cmd_words(cmd_str);
let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
let fifo_counter = self.process_sub_counter;
self.process_sub_counter += 1;
let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
let _ = fs::remove_file(&fifo_path);
if nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU).is_err() {
return String::new();
}
let fifo_clone = fifo_path.clone();
if !words.is_empty() {
let cmd_name = words[0].clone();
let args: Vec<String> = words[1..].to_vec();
self.worker_pool.submit(move || {
if let Ok(fifo) = fs::File::open(&fifo_clone) {
let _ = Command::new(&cmd_name)
.args(&args)
.stdin(fifo)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
}
let _ = fs::remove_file(&fifo_clone);
});
}
fifo_path
}
pub fn run_command_substitution(&mut self, cmd_str: &str) -> String {
let trimmed = cmd_str.trim_start();
if let Some(rest) = trimmed.strip_prefix('<').filter(|s| !s.starts_with('<')) {
let filename = rest.trim();
let resolved = if filename.contains('$') || filename.starts_with('~') {
crate::ported::subst::singsub(filename)
} else {
filename.to_string()
};
let resolved = resolved.to_string();
match std::fs::read_to_string(&resolved) {
Ok(contents) => {
return contents.trim_end_matches('\n').to_string();
}
Err(_) => {
eprintln!("zshrs:1: no such file or directory: {}", resolved);
return String::new();
}
}
}
let (read_fd, write_fd) = {
let mut fds = [0i32; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return String::new();
}
(fds[0], fds[1])
};
let saved_stdout = unsafe { libc::dup(libc::STDOUT_FILENO) };
if saved_stdout < 0 {
unsafe {
libc::close(read_fd);
libc::close(write_fd);
}
return String::new();
}
unsafe {
libc::dup2(write_fd, libc::STDOUT_FILENO);
libc::close(write_fd);
}
crate::ported::prompt::cmdpush(crate::ported::zsh_h::CS_CMDSUBST as u8); let saved_lineno = crate::ported::params::getsparam("LINENO");
let outer_lineno: u64 = self
.scalar("LINENO")
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let saved_errflag = errflag.load(Ordering::Relaxed);
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
crate::ported::parse::parse_init(cmd_str);
let parsed = crate::ported::parse::parse();
let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
errflag.store(saved_errflag, Ordering::Relaxed);
let prog = if parse_failed { None } else { Some(parsed) };
let mut cmd_status: Option<i32> = None;
if let Some(prog) = prog {
let mut compiler = crate::compile_zsh::ZshCompiler::new();
compiler.lineno_addend = outer_lineno.saturating_sub(1);
let chunk = compiler.compile(&prog);
if !chunk.ops.is_empty() {
let mut vm = fusevm::VM::new(chunk);
register_builtins(&mut vm);
vm.set_shell_host(Box::new(ZshrsHost));
vm.last_status = self.last_status();
let _ctx = ExecutorContext::enter(self);
let _ = vm.run();
cmd_status = Some(vm.last_status);
}
}
if let Some(ln) = saved_lineno {
self.set_scalar("LINENO".to_string(), ln);
}
crate::ported::prompt::cmdpop();
if let Some(status) = cmd_status {
self.set_last_status(status);
} else {
self.set_last_status(0);
}
let _ = io::stdout().flush();
unsafe {
libc::dup2(saved_stdout, libc::STDOUT_FILENO);
libc::close(saved_stdout);
}
let read_file = unsafe { std::fs::File::from_raw_fd(read_fd) };
let mut output = String::new();
let _ = std::io::BufReader::new(read_file).read_to_string(&mut output);
while output.ends_with('\n') {
output.pop();
}
output
}
}
impl Default for ShellExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_echo() {
let mut exec = ShellExecutor::new();
let status = exec.execute_script("true").unwrap();
assert_eq!(status, 0);
}
#[test]
fn test_if_true() {
let mut exec = ShellExecutor::new();
let status = exec.execute_script("if true; then true; fi").unwrap();
assert_eq!(status, 0);
}
#[test]
fn test_if_false() {
let mut exec = ShellExecutor::new();
let status = exec
.execute_script("if false; then true; else false; fi")
.unwrap();
assert_eq!(status, 1);
}
#[test]
fn test_for_loop() {
let mut exec = ShellExecutor::new();
exec.execute_script("for i in a b c; do true; done")
.unwrap();
assert_eq!(exec.last_status(), 0);
}
#[test]
fn test_and_list() {
let mut exec = ShellExecutor::new();
let status = exec.execute_script("true && true").unwrap();
assert_eq!(status, 0);
let status = exec.execute_script("true && false").unwrap();
assert_eq!(status, 1);
}
#[test]
fn test_or_list() {
let mut exec = ShellExecutor::new();
let status = exec.execute_script("false || true").unwrap();
assert_eq!(status, 0);
}
}
#[cfg(feature = "recorder")]
pub(crate) fn emit_path_or_assign(
name: &str,
values: &[String],
attrs: crate::recorder::ParamAttrs,
is_append: bool,
ctx: &crate::recorder::RecordCtx,
) {
let lower = name.to_ascii_lowercase();
let kind_name: Option<&'static str> = match lower.as_str() {
"path" => Some("path"),
"fpath" => Some("fpath"),
"manpath" => Some("manpath"),
"module_path" => Some("module_path"),
"cdpath" => Some("cdpath"),
_ => None,
};
match kind_name {
Some(k) => {
for v in values {
crate::recorder::emit_path_mod(v, k, ctx.clone());
if k == "fpath" {
crate::recorder::discover_completions_in_fpath_dir(v, ctx);
}
}
}
None => {
crate::recorder::emit_array_assign(
name,
values.to_vec(),
attrs,
is_append,
ctx.clone(),
);
}
}
}
#[cfg(feature = "recorder")]
impl ShellExecutor {}
#[cfg(feature = "recorder")]
impl ShellExecutor {}
impl ShellExecutor {
pub(crate) fn add_hook(&mut self, _hook: &str, _func: &str) {}
pub(crate) fn delete_hook(&mut self, _hook: &str, _func: &str) {}
}
impl ShellExecutor {
pub(crate) fn is_builtin(&self, name: &str) -> bool {
BUILTIN_NAMES.contains(name) || name.starts_with('_')
}
pub(crate) fn find_in_path(&self, name: &str) -> Option<String> {
if let Some(p) = crate::ported::hashtable::cmdnamtab_lock()
.read().ok()
.and_then(|t| t.get_full_path(name))
{
return Some(p.display().to_string());
}
crate::ported::builtin::findcmd(name, 0, 0) }
pub fn zfork(&mut self, flags: ForkFlags) -> std::io::Result<ForkResult> {
let can_background = crate::ported::options::opt_state_get("monitor").unwrap_or(false);
unsafe {
match libc::fork() {
-1 => Err(std::io::Error::last_os_error()),
0 => {
if !flags.contains(ForkFlags::NOJOB) && can_background {
let pid = libc::getpid();
if flags.contains(ForkFlags::NEWGRP) {
libc::setpgid(0, 0);
}
if flags.contains(ForkFlags::FGTTY) {
libc::tcsetpgrp(0, pid);
}
}
if !flags.contains(ForkFlags::KEEPSIGS) {
self.reset_signals();
}
Ok(ForkResult::Child)
}
pid => {
if !flags.contains(ForkFlags::NOJOB) {
self.add_child_process(pid);
}
Ok(ForkResult::Parent(pid))
}
}
}
}
fn add_child_process(&mut self, pid: i32) {
self.set_scalar("!".to_string(), pid.to_string());
}
fn reset_signals(&self) {
unsafe {
libc::signal(libc::SIGINT, libc::SIG_DFL);
libc::signal(libc::SIGQUIT, libc::SIG_DFL);
libc::signal(libc::SIGTERM, libc::SIG_DFL);
libc::signal(libc::SIGTSTP, libc::SIG_DFL);
libc::signal(libc::SIGTTIN, libc::SIG_DFL);
libc::signal(libc::SIGTTOU, libc::SIG_DFL);
libc::signal(libc::SIGCHLD, libc::SIG_DFL);
}
}
pub fn zexecve(&self, cmd: &str, args: &[String]) -> ! {
let c_cmd = CString::new(cmd).expect("CString::new failed");
let c_args: Vec<CString> = std::iter::once(c_cmd.clone())
.chain(args.iter().map(|s| CString::new(s.as_str()).unwrap()))
.collect();
let c_argv: Vec<*const libc::c_char> = c_args
.iter()
.map(|s| s.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
let env_vars: Vec<CString> = std::env::vars()
.map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
.collect();
let c_envp: Vec<*const libc::c_char> = env_vars
.iter()
.map(|s| s.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
unsafe {
libc::execve(c_cmd.as_ptr(), c_argv.as_ptr(), c_envp.as_ptr());
eprintln!(
"zshrs: exec failed: {}: {}",
cmd,
std::io::Error::last_os_error()
);
std::process::exit(127);
}
}
pub fn entersubsh(&mut self, flags: SubshellFlags) {
let level = self
.get_variable("ZSH_SUBSHELL")
.parse::<i32>()
.unwrap_or(0);
self.set_scalar("ZSH_SUBSHELL".to_string(), (level + 1).to_string());
if flags.contains(SubshellFlags::NOMONITOR) {
crate::ported::options::opt_state_set("monitor", false);
}
if !flags.contains(SubshellFlags::KEEPFDS) {
self.close_extra_fds();
}
if !flags.contains(SubshellFlags::KEEPTRAPS) {
self.reset_traps();
}
}
fn close_extra_fds(&self) {
for fd in 10..256 {
unsafe {
libc::close(fd);
}
}
}
fn reset_traps(&mut self) {
self.traps.clear();
}
}
use std::os::unix::fs::MetadataExt;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, Default)]
pub struct ForkFlags: u32 {
const NOJOB = 1 << 0; const NEWGRP = 1 << 1; const FGTTY = 1 << 2; const KEEPSIGS = 1 << 3; }
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, Default)]
pub struct SubshellFlags: u32 {
const NOMONITOR = 1 << 0; const KEEPFDS = 1 << 1; const KEEPTRAPS = 1 << 2; }
}
#[derive(Debug)]
pub enum ForkResult {
Parent(i32), Child,
}
#[derive(Debug, Clone, Copy)]
pub enum RedirMode {
Dup,
Close,
}
#[derive(Debug, Clone, Copy)]
pub enum BuiltinType {
Normal,
Disabled,
}
#[allow(unused_variables, dead_code)]
impl ShellExecutor {
fn _empty_ops() -> crate::ported::zsh_h::options {
options { ind: [0u8; MAX_OPS], args: Vec::new(),
argscount: 0, argsalloc: 0 }
}
pub(crate) fn dispatch_pending_traps(&mut self) {}
pub(crate) fn builtin_pwd_with_args(&mut self, args: &[String]) -> i32 {
let ops = Self::_empty_ops();
crate::ported::builtin::bin_pwd("pwd", args, &ops, 0)
}
pub(crate) fn bin_zcompile(&mut self, args: &[String]) -> i32 {
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let mut positional: Vec<String> = Vec::new();
let mut consume_opts = true;
for a in args.iter() {
if consume_opts && a == "--" { consume_opts = false; continue; }
if consume_opts && a.starts_with('-') && a.len() > 1 {
for c in a.bytes().skip(1) {
let idx = c as usize;
if idx < MAX_OPS {
ops.ind[idx] |= 1; }
}
continue;
}
consume_opts = false;
positional.push(a.clone());
}
crate::ported::parse::bin_zcompile("zcompile", &positional, &ops, 0)
}
pub(crate) fn builtin_echo(&mut self, args: &[String], _redir: &[crate::parse::Redirect]) -> i32 {
let bn_idx = crate::ported::builtin::BUILTINS.iter()
.position(|b| b.node.nam == "echo");
match bn_idx {
Some(idx) => {
let bn_static: &'static crate::ported::zsh_h::builtin =
&crate::ported::builtin::BUILTINS[idx];
let bn_ptr = bn_static as *const _ as *mut _;
crate::ported::builtin::execbuiltin(args.to_vec(), Vec::new(), bn_ptr)
}
None => {
let ops = Self::_empty_ops();
crate::ported::builtin::bin_print("echo", args, &ops,
crate::ported::builtin::BIN_ECHO)
}
}
}
pub(crate) fn builtin_printf(&mut self, args: &[String]) -> i32 {
let bn_idx = crate::ported::builtin::BUILTINS.iter()
.position(|b| b.node.nam == "printf");
match bn_idx {
Some(idx) => {
let bn_static: &'static crate::ported::zsh_h::builtin =
&crate::ported::builtin::BUILTINS[idx];
let bn_ptr = bn_static as *const _ as *mut _;
crate::ported::builtin::execbuiltin(args.to_vec(), Vec::new(), bn_ptr)
}
None => {
let ops = Self::_empty_ops();
crate::ported::builtin::bin_print("printf", args, &ops,
crate::ported::builtin::BIN_PRINTF)
}
}
}
pub(crate) fn builtin_local(&mut self, args: &[String]) -> i32 {
self.dispatch_typeset_as("local", args)
}
pub(crate) fn builtin_export(&mut self, args: &[String]) -> i32 {
self.dispatch_typeset_as("export", args)
}
pub(crate) fn builtin_readonly(&mut self, args: &[String]) -> i32 {
self.dispatch_typeset_as("readonly", args)
}
pub(crate) fn builtin_declare(&mut self, args: &[String]) -> i32 {
self.dispatch_typeset_as("declare", args)
}
pub(crate) fn builtin_typeset_named(&mut self, name: &str, args: &[String]) -> i32 {
self.dispatch_typeset_as(name, args)
}
pub(crate) fn builtin_integer(&mut self, args: &[String]) -> i32 {
self.dispatch_typeset_as("integer", args)
}
pub(crate) fn builtin_float(&mut self, args: &[String]) -> i32 {
self.dispatch_typeset_as("float", args)
}
fn dispatch_typeset_as(&mut self, name: &str, args: &[String]) -> i32 {
let bn_idx = crate::ported::builtin::BUILTINS.iter()
.position(|b| b.node.nam == name);
if let Some(idx) = bn_idx {
let bn_static: &'static crate::ported::zsh_h::builtin =
&crate::ported::builtin::BUILTINS[idx];
let bn_ptr = bn_static as *const _ as *mut _;
return crate::ported::builtin::execbuiltin(args.to_vec(), Vec::new(), bn_ptr);
}
for a in args {
if let Some(eq) = a.find('=') {
std::env::set_var(&a[..eq], &a[eq+1..]);
}
}
0
}
}
use crate::ported::utils::{zwarn, zwarnnam, zerr, zerrnam};
use crate::ported::params::*;
use crate::ported::options::*;
use crate::ported::hist::*;
use crate::ported::pattern::*;
use crate::ported::prompt::*;
use crate::ported::subst::*;
use crate::ported::math::*;
use crate::ported::jobs::*;
use crate::ported::glob::*;
use crate::ported::module::*;
use crate::ported::signals::*;
use crate::ported::modules::cap::*;
use crate::ported::modules::tcp::bin_ztcp;
use crate::ported::modules::termcap::bin_echotc;
use crate::ported::modules::terminfo::*;
use crate::fusevm_bridge::with_executor;
use ::regex::{Regex, RegexBuilder, Error as RegexError};
impl crate::ported::exec::ShellExecutor {
pub(crate) fn has_extglob_pattern(&self, pattern: &str) -> bool {
let chars: Vec<char> = pattern.chars().collect();
for i in 0..chars.len().saturating_sub(1) {
if (chars[i] == '?'
|| chars[i] == '*'
|| chars[i] == '+'
|| chars[i] == '@'
|| chars[i] == '!')
&& chars[i + 1] == '('
{
return true;
}
}
false
}
pub(crate) fn extract_extglob_inner(&self, chars: &[char], start: usize) -> (String, usize) {
let mut inner = String::new();
let mut depth = 1;
let mut i = start;
while i < chars.len() && depth > 0 {
if chars[i] == '(' {
depth += 1;
} else if chars[i] == ')' {
depth -= 1;
if depth == 0 {
return (inner, i);
}
}
inner.push(chars[i]);
i += 1;
}
(inner, i)
}
pub(crate) fn extglob_inner_to_regex(&self, inner: &str) -> String {
let alternatives: Vec<String> = inner
.split('|')
.map(|alt| {
let mut result = String::new();
for c in alt.chars() {
match c {
'*' => result.push_str(".*"),
'?' => result.push('.'),
'.' => result.push_str("\\."),
'^' | '$' | '(' | ')' | '{' | '}' | '\\' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
result
})
.collect();
alternatives.join("|")
}
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn all_zsh_options() -> Vec<&'static str> {
crate::ported::options::ZSH_OPTIONS_SET.iter().copied().collect()
}
pub(crate) fn default_options() -> HashMap<String, bool> {
let mut opts = HashMap::new();
for opt in Self::all_zsh_options() {
opts.insert(opt.to_string(), false);
}
let defaults_on = [
"aliases",
"alwayslastprompt",
"appendhistory",
"autolist",
"automenu",
"autoparamkeys",
"autoparamslash",
"autoremoveslash",
"badpattern",
"banghist",
"bareglobqual",
"beep",
"bgnice",
"caseglob",
"casematch",
"checkjobs",
"checkrunningjobs",
"clobber",
"debugbeforecmd",
"equals",
"evallineno",
"exec",
"flowcontrol",
"functionargzero",
"glob",
"globalexport",
"globalrcs",
"hashcmds",
"hashdirs",
"hashlistall",
"histbeep",
"histsavebycopy",
"hup",
"listambiguous",
"listbeep",
"listtypes",
"monitor",
"multibyte",
"multifuncdef",
"multios",
"nomatch",
"notify",
"promptcr",
"promptpercent",
"promptsp",
"rcs",
"shortloops",
"unset",
"zle",
];
for opt in defaults_on {
opts.insert(opt.to_string(), true);
}
opts
}
pub(crate) fn default_on_options() -> &'static [&'static str] {
&[
"aliases",
"alwayslastprompt",
"appendhistory",
"autolist",
"automenu",
"autoparamkeys",
"autoparamslash",
"autoremoveslash",
"badpattern",
"banghist",
"bareglobqual",
"beep",
"bgnice",
"caseglob",
"casematch",
"checkjobs",
"checkrunningjobs",
"clobber",
"debugbeforecmd",
"equals",
"evallineno",
"exec",
"flowcontrol",
"functionargzero",
"glob",
"globalexport",
"globalrcs",
"hashcmds",
"hashdirs",
"hashlistall",
"histbeep",
"histsavebycopy",
"hup",
"listambiguous",
"listbeep",
"listtypes",
"monitor",
"multibyte",
"multifuncdef",
"multios",
"nomatch",
"notify",
"promptcr",
"promptpercent",
"promptsp",
"rcs",
"shortloops",
"unset",
"zle",
]
}
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn parse_subscript_range(&self, s: &str, len: usize) -> Option<(usize, usize)> {
if s.is_empty() || len == 0 {
return None;
}
let parts: Vec<&str> = s.split(',').collect();
let parse_idx = |idx_str: &str| -> Option<usize> {
let idx: i64 = idx_str.trim().parse().ok()?;
if idx < 0 {
let abs = (-idx) as usize;
if abs > len {
None
} else {
Some(len - abs)
}
} else if idx == 0 {
Some(0)
} else {
Some((idx as usize).saturating_sub(1).min(len))
}
};
match parts.len() {
1 => {
let idx = parse_idx(parts[0])?;
Some((idx, idx + 1))
}
2 => {
let start = parse_idx(parts[0])?;
let end = parse_idx(parts[1])?.saturating_add(1);
Some((start.min(end), start.max(end)))
}
_ => None,
}
}
pub fn get_special_array_value(&self, array_name: &str, key: &str) -> Option<String> {
match array_name {
"mapfile" => {
if key == "@" || key == "*" {
let mut files: Vec<String> = Vec::new();
if let Ok(rd) = std::fs::read_dir(".") {
for entry in rd.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(name) =
path.file_name().and_then(|n| n.to_str())
{
files.push(name.to_string());
}
}
}
}
return Some(files.join(" "));
}
Some(crate::modules::mapfile::get_contents(key).unwrap_or_default())
}
"errnos" => {
let table = crate::modules::system::ERRNO_NAMES;
if key == "@" || key == "*" {
return Some(
table
.iter()
.map(|(n, _)| (*n).to_string())
.collect::<Vec<_>>()
.join(" "),
);
}
if let Ok(n) = key.parse::<i64>() {
let len = table.len() as i64;
let pos = if n > 0 {
(n - 1) as usize
} else if n < 0 {
let p = len + n;
if p < 0 {
return Some(String::new());
}
p as usize
} else {
return Some(String::new());
};
if let Some((name, _)) = table.get(pos) {
return Some((*name).to_string());
}
}
Some(String::new())
}
"sysparams" => {
let pid = std::process::id().to_string();
let ppid = unsafe { libc::getppid() }.to_string();
if key == "@" || key == "*" {
return Some(format!("{} {}", pid, ppid));
}
Some(match key {
"pid" => pid,
"ppid" => ppid,
"procsubstpid" => "0".to_string(),
_ => String::new(),
})
}
"options" => {
if key == "@" || key == "*" {
let opts: Vec<String> = crate::ported::options::opt_state_snapshot()
.iter()
.map(|(k, v)| format!("{}={}", k, if *v { "on" } else { "off" }))
.collect();
return Some(opts.join(" "));
}
let opt_name = key.to_lowercase().replace('_', "");
let is_on = crate::ported::options::opt_state_get(&opt_name).unwrap_or(false);
Some(if is_on {
"on".to_string()
} else {
"off".to_string()
})
}
"aliases" => {
if let Ok(tab) = crate::ported::hashtable::aliastab_lock().read() {
if key == "@" || key == "*" {
let mut names: Vec<&String> = tab.iter().map(|(n, _)| n).collect();
names.sort();
let vals: Vec<String> = names.iter()
.filter_map(|n| tab.get(n).map(|a| a.text.clone()))
.collect();
return Some(vals.join(" "));
}
return Some(tab.get(key).map(|a| a.text.clone()).unwrap_or_default());
}
Some(self.alias(key).unwrap_or_default())
}
"galiases" => {
if key == "@" || key == "*" {
let entries = self.global_alias_entries();
let vals: Vec<String> = entries.into_iter().map(|(_, v)| v).collect();
return Some(vals.join(" "));
}
Some(self.global_alias(key).unwrap_or_default())
}
"saliases" => {
if key == "@" || key == "*" {
let entries = self.suffix_alias_entries();
let vals: Vec<String> = entries.into_iter().map(|(_, v)| v).collect();
return Some(vals.join(" "));
}
Some(self.suffix_alias(key).unwrap_or_default())
}
"terminfo" => {
Some(crate::modules::terminfo::getterminfo(key).unwrap_or_default())
}
"termcap" => Some(crate::modules::termcap::gettermcap(key).unwrap_or_default()),
"functions" => {
if key == "@" || key == "*" {
return Some(self.function_names().join(" "));
}
let text = self.function_definition_text(key)?;
let formatted = FuncBodyFmt::render(text.trim());
Some(format!("\t{}", formatted))
}
"functions_source" => {
if key == "@" || key == "*" {
let _ = self.function_names();
return Some(String::new());
}
{ let _ = key; Some(String::new()) }
}
"commands" => {
if key == "@" || key == "*" {
let path_var = env::var("PATH").unwrap_or_default();
let mut seen: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut names: Vec<String> = Vec::new();
if let Ok(tab) = crate::ported::hashtable::cmdnamtab_lock().read() {
for (k, _) in tab.iter() {
if seen.insert(k.clone()) {
names.push(k.clone());
}
}
}
for dir in path_var.split(':') {
if dir.is_empty() {
continue;
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if let Ok(name) = entry.file_name().into_string() {
if seen.insert(name.clone()) {
names.push(name);
}
}
}
}
}
names.sort();
return Some(names.join(" "));
}
if let Some(path) = self.find_in_path(key) {
Some(path)
} else {
Some(String::new())
}
}
"builtins" => {
let builtins: Vec<&str> = crate::exec::BUILTIN_NAMES
.iter().map(|s| s.as_str()).collect();
if key == "@" || key == "*" {
return Some(builtins.join(" "));
}
if builtins.iter().any(|b| *b == key) {
Some("defined".to_string())
} else {
Some(String::new())
}
}
"parameters" => {
if key == "@" || key == "*" {
let mut names: std::collections::BTreeSet<String> =
if let Ok(tab) = crate::ported::params::paramtab().read() {
tab.keys().cloned().collect()
} else {
std::collections::BTreeSet::new()
};
if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
names.extend(m.keys().cloned());
}
let v: Vec<String> = names.into_iter().collect();
return Some(v.join(" "));
}
let flags = self.param_flags(key) as u32;
if flags != 0 || self.array(key).is_some() || self.assoc(key).is_some() {
let base = if flags & PM_INTEGER != 0 { "integer" }
else if flags & (PM_EFLOAT | PM_FFLOAT) != 0 { "float" }
else if flags & PM_HASHED != 0 || self.assoc(key).is_some() { "association" }
else if flags & PM_ARRAY != 0 || self.array(key).is_some() { "array" }
else { "scalar" };
let mut out = String::from(base);
if flags & PM_LEFT != 0 { out.push_str("-left"); }
if flags & PM_RIGHT_B != 0 { out.push_str("-right_blanks"); }
if flags & PM_RIGHT_Z != 0 { out.push_str("-right_zeros"); }
if flags & PM_LOWER != 0 { out.push_str("-lower"); }
if flags & PM_UPPER != 0 { out.push_str("-upper"); }
if flags & PM_READONLY != 0 { out.push_str("-readonly"); }
if flags & PM_EXPORTED != 0 { out.push_str("-export"); }
return Some(out);
}
if self.has_assoc(key) {
Some("association".to_string())
} else if self.has_array(key) {
Some("array".to_string())
} else if self.has_scalar(key) || std::env::var(key).is_ok() {
Some("scalar".to_string())
} else {
Some(String::new())
}
}
"nameddirs" => {
let tab = crate::ported::hashnameddir::nameddirtab();
if key == "@" || key == "*" {
let snapshot: Vec<(String, String)> = tab.lock().ok()
.map(|g| g.iter().map(|(k, v)| (k.clone(), v.dir.clone())).collect())
.unwrap_or_default();
let mut keys: Vec<&(String, String)> = snapshot.iter().collect();
keys.sort_by(|a, b| a.0.cmp(&b.0));
let vals: Vec<String> = keys.iter().map(|(_, v)| v.clone()).collect();
return Some(vals.join(" "));
}
Some(
tab.lock().ok()
.and_then(|g| g.get(key).map(|nd| nd.dir.clone()))
.unwrap_or_default(),
)
}
"userdirs" => {
#[cfg(unix)]
{
if key == "@" || key == "*" {
let mut homes: Vec<String> = Vec::new();
unsafe {
libc::setpwent();
loop {
let pwd = libc::getpwent();
if pwd.is_null() {
break;
}
let dir = CStr::from_ptr((*pwd).pw_dir);
homes.push(dir.to_string_lossy().to_string());
}
libc::endpwent();
}
homes.sort();
homes.dedup();
return Some(homes.join(" "));
}
if let Ok(name) = CString::new(key) {
unsafe {
let pwd = libc::getpwnam(name.as_ptr());
if !pwd.is_null() {
let dir = CStr::from_ptr((*pwd).pw_dir);
return Some(dir.to_string_lossy().to_string());
}
}
}
}
Some(String::new())
}
"usergroups" => {
#[cfg(unix)]
{
if key == "@" || key == "*" {
let mut gids: Vec<String> = Vec::new();
unsafe {
libc::setgrent();
loop {
let grp = libc::getgrent();
if grp.is_null() {
break;
}
let name = CStr::from_ptr((*grp).gr_name);
gids.push(name.to_string_lossy().to_string());
}
libc::endgrent();
}
gids.sort();
gids.dedup();
return Some(gids.join(" "));
}
if let Ok(name) = CString::new(key) {
unsafe {
let grp = libc::getgrnam(name.as_ptr());
if !grp.is_null() {
return Some((*grp).gr_gid.to_string());
}
}
}
}
Some(String::new())
}
"dirstack" => {
let dirs = crate::ported::modules::parameter::DIRSTACK
.lock()
.map(|g| g.clone())
.unwrap_or_default();
if key == "@" || key == "*" {
return Some(dirs.join(" "));
}
if let Ok(idx) = key.parse::<usize>() {
Some(dirs.get(idx).cloned().unwrap_or_default())
} else {
Some(String::new())
}
}
"jobstates" => {
if key == "@" || key == "*" {
let states: Vec<String> = self
.jobs
.iter()
.map(|(id, job)| format!("{}:{:?}", id, job.state))
.collect();
return Some(states.join(" "));
}
if let Ok(id) = key.parse::<usize>() {
if let Some(job) = self.jobs.get(id) {
return Some(format!("{:?}", job.state));
}
}
Some(String::new())
}
"jobtexts" => {
if key == "@" || key == "*" {
let texts: Vec<String> = self
.jobs
.iter()
.map(|(_, job)| job.command.clone())
.collect();
return Some(texts.join(" "));
}
if let Ok(id) = key.parse::<usize>() {
if let Some(job) = self.jobs.get(id) {
return Some(job.command.clone());
}
}
Some(String::new())
}
"jobdirs" => {
let pwd = self
.scalar("PWD")
.or_else(|| env::var("PWD").ok())
.unwrap_or_default();
if key == "@" || key == "*" {
let n = self.jobs.iter().count();
return Some(vec![pwd; n].join(" "));
}
if let Ok(id) = key.parse::<usize>() {
if self.jobs.get(id).is_some() {
return Some(pwd);
}
}
Some(String::new())
}
"history" => {
if key == "@" || key == "*" {
if let Some(ref engine) = self.history {
if let Ok(entries) = engine.recent(100) {
let cmds: Vec<String> =
entries.iter().map(|e| e.command.clone()).collect();
return Some(cmds.join("\n"));
}
}
return Some(String::new());
}
if let Ok(num) = key.parse::<usize>() {
if let Some(ref engine) = self.history {
if let Ok(Some(entry)) = engine.get_by_offset(num.saturating_sub(1)) {
return Some(entry.command);
}
}
}
Some(String::new())
}
"historywords" => {
if let Some(ref engine) = self.history {
if let Ok(entries) = engine.recent(100) {
let words: Vec<String> = entries
.iter()
.flat_map(|e| {
e.command
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
.collect();
if key == "@" || key == "*" {
return Some(words.join(" "));
}
if let Ok(idx) = key.parse::<usize>() {
if idx >= 1 && idx <= words.len() {
return Some(words[idx - 1].clone());
}
}
}
}
Some(String::new())
}
"modules" => {
const ALWAYS_LOADED: &[&str] = &[
"zsh/parameter",
"zsh/zutil",
"zsh/complete",
"zsh/complist",
"zsh/zle",
"zsh/main",
"zsh/files",
];
let user_loaded: Vec<String> = crate::ported::options::opt_state_snapshot()
.iter()
.filter_map(|(k, v)| {
if *v {
k.strip_prefix("_module_").map(|s| s.to_string())
} else {
None
}
})
.collect();
if key == "@" || key == "*" {
let mut all: Vec<String> = ALWAYS_LOADED
.iter()
.map(|s| s.to_string())
.chain(user_loaded.iter().cloned())
.collect();
all.sort();
all.dedup();
return Some(all.join(" "));
}
if ALWAYS_LOADED.contains(&key)
|| crate::ported::options::opt_state_get(&format!("_module_{}", key))
.unwrap_or(false)
{
Some("loaded".to_string())
} else {
Some(String::new())
}
}
"reswords" => {
let reswords = [
"do",
"done",
"esac",
"then",
"elif",
"else",
"fi",
"for",
"case",
"if",
"while",
"function",
"repeat",
"time",
"until",
"select",
"coproc",
"nocorrect",
"foreach",
"end",
"in",
];
if key == "@" || key == "*" {
return Some(reswords.join(" "));
}
if let Ok(idx) = key.parse::<usize>() {
Some(reswords.get(idx).map(|s| s.to_string()).unwrap_or_default())
} else {
Some(String::new())
}
}
"patchars" => {
let patchars = ["?", "*", "[", "]", "^", "#", "~", "(", ")", "|"];
if key == "@" || key == "*" {
return Some(patchars.join(" "));
}
if let Ok(idx) = key.parse::<usize>() {
Some(patchars.get(idx).map(|s| s.to_string()).unwrap_or_default())
} else {
Some(String::new())
}
}
"funcstack" => {
if let Some(stack) = self.array("funcstack") {
if key == "@" || key == "*" {
return Some(stack.join(" "));
}
if let Ok(idx) = key.parse::<usize>() {
if idx >= 1 && idx <= stack.len() {
return Some(stack[idx - 1].clone());
}
}
}
Some(String::new())
}
"functrace" => {
if let Some(stack) = self.array("funcstack") {
let synth: Vec<String> = stack.iter().map(|n| format!("{}:0", n)).collect();
if key == "@" || key == "*" {
return Some(synth.join(" "));
}
if let Ok(idx) = key.parse::<usize>() {
if idx >= 1 && idx <= synth.len() {
return Some(synth[idx - 1].clone());
}
}
}
Some(String::new())
}
"funcfiletrace" | "funcsourcetrace" => {
Some(String::new())
}
"dis_builtins" => {
let disabled: Vec<String> = crate::ported::options::opt_state_snapshot()
.iter()
.filter_map(|(k, v)| {
if *v {
k.strip_prefix("_disabled_").map(|s| s.to_string())
} else {
None
}
})
.collect();
if key == "@" || key == "*" {
let mut sorted = disabled.clone();
sorted.sort();
return Some(sorted.join(" "));
}
if disabled.iter().any(|d| d == key) {
Some("defined".to_string())
} else {
Some(String::new())
}
}
"dis_aliases"
| "dis_galiases"
| "dis_saliases"
| "dis_functions"
| "dis_functions_source"
| "dis_reswords"
| "dis_patchars" => Some(String::new()),
"widgets" => {
if key == "@" || key == "*" {
let mut names = listwidgets();
names.sort();
return Some(names.join(" "));
}
if let Some(target) = getwidgettarget(key) {
if target == key {
Some("builtin".to_string())
} else {
Some(format!("user:{}", target))
}
} else {
Some(String::new())
}
}
"keymaps" => {
const KEYMAPS: &[&str] = &[
"main",
"emacs",
"viins",
"vicmd",
"isearch",
"command",
"menuselect",
];
if key == "@" || key == "*" {
return Some(KEYMAPS.join(" "));
}
if KEYMAPS.contains(&key) {
Some("1".to_string())
} else {
Some(String::new())
}
}
"signals" => {
let map: &[(i32, &str)] = &[
(libc::SIGHUP, "HUP"),
(libc::SIGINT, "INT"),
(libc::SIGQUIT, "QUIT"),
(libc::SIGILL, "ILL"),
(libc::SIGTRAP, "TRAP"),
(libc::SIGABRT, "ABRT"),
#[cfg(target_os = "macos")]
(libc::SIGEMT, "EMT"),
(libc::SIGFPE, "FPE"),
(libc::SIGKILL, "KILL"),
(libc::SIGBUS, "BUS"),
(libc::SIGSEGV, "SEGV"),
(libc::SIGSYS, "SYS"),
(libc::SIGPIPE, "PIPE"),
(libc::SIGALRM, "ALRM"),
(libc::SIGTERM, "TERM"),
(libc::SIGURG, "URG"),
(libc::SIGSTOP, "STOP"),
(libc::SIGTSTP, "TSTP"),
(libc::SIGCONT, "CONT"),
(libc::SIGCHLD, "CHLD"),
(libc::SIGTTIN, "TTIN"),
(libc::SIGTTOU, "TTOU"),
(libc::SIGIO, "IO"),
(libc::SIGXCPU, "XCPU"),
(libc::SIGXFSZ, "XFSZ"),
(libc::SIGVTALRM, "VTALRM"),
(libc::SIGPROF, "PROF"),
(libc::SIGWINCH, "WINCH"),
#[cfg(target_os = "macos")]
(libc::SIGINFO, "INFO"),
(libc::SIGUSR1, "USR1"),
(libc::SIGUSR2, "USR2"),
];
if key == "@" || key == "*" {
let max = map.iter().map(|(n, _)| *n).max().unwrap_or(0) as usize;
let mut slots: Vec<String> = vec![String::new(); max];
for (n, name) in map {
if (*n as usize) >= 1 && (*n as usize) <= max {
slots[*n as usize - 1] = (*name).to_string();
}
}
return Some(slots.join(" "));
}
if let Ok(n) = key.parse::<i32>() {
for (sig_num, name) in map {
if *sig_num == n {
return Some((*name).to_string());
}
}
}
Some(String::new())
}
_ => None,
}
}
pub(crate) fn get_variable(&self, name: &str) -> String {
match name {
"" => String::new(), "$" => std::process::id().to_string(),
"@" | "*" => {
let sep = self
.scalar("IFS")
.and_then(|s| s.chars().next())
.unwrap_or(' ');
self.pparams().join(&sep.to_string())
}
"#" | "#@" | "#*" => self.pparams().len().to_string(),
"ARGC" => self.pparams().len().to_string(),
"?" | "status" => self.last_status().to_string(),
"!" => self
.scalar("!")
.unwrap_or_else(|| "0".to_string()),
"-" => {
let mut letters = String::from("569X");
let opt = |n: &str| crate::ported::options::opt_state_get(n).unwrap_or(false);
if opt("errexit") {
letters.push('e');
}
if !opt("rcs") {
letters.push('f');
}
if opt("login") {
letters.push('l');
}
if opt("nounset") {
letters.push('u');
}
if opt("xtrace") {
letters.push('x');
}
if opt("verbose") {
letters.push('v');
}
if opt("noexec") {
letters.push('n');
}
if opt("hashall") {
letters.push('h');
}
letters
}
"EUID" => unsafe { libc::geteuid() }.to_string(),
"UID" => unsafe { libc::getuid() }.to_string(),
"EGID" => unsafe { libc::getegid() }.to_string(),
"GID" => unsafe { libc::getgid() }.to_string(),
"PPID" => unsafe { libc::getppid() }.to_string(),
"ZSH_SUBSHELL" => self
.scalar("ZSH_SUBSHELL")
.unwrap_or_else(|| "0".to_string()),
"HOST" => {
let mut buf = [0u8; 256];
let r = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) };
if r == 0 {
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..nul]).into_owned()
} else {
String::new()
}
}
"OSTYPE" => {
let mut u: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut u) } == 0 {
let sysname = unsafe { std::ffi::CStr::from_ptr(u.sysname.as_ptr()) }
.to_string_lossy()
.to_lowercase();
let release = unsafe { std::ffi::CStr::from_ptr(u.release.as_ptr()) }
.to_string_lossy()
.to_string();
format!("{}{}", sysname, release)
} else {
std::env::consts::OS.to_string()
}
}
"MACHTYPE" => {
let mut u: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut u) } == 0 {
let m = unsafe { std::ffi::CStr::from_ptr(u.machine.as_ptr()) }
.to_string_lossy()
.to_string();
if m == "aarch64" || m == "arm64" {
"arm".to_string()
} else {
m
}
} else {
std::env::consts::ARCH.to_string()
}
}
"CPUTYPE" => {
let mut u: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut u) } == 0 {
unsafe { std::ffi::CStr::from_ptr(u.machine.as_ptr()) }
.to_string_lossy()
.to_string()
} else {
std::env::consts::ARCH.to_string()
}
}
"VENDOR" => {
if cfg!(target_os = "macos") {
"apple".to_string()
} else if cfg!(target_os = "linux") {
"unknown".to_string()
} else {
"pc".to_string()
}
}
"HOSTNAME" => {
let mut buf = [0u8; 256];
let r = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) };
if r == 0 {
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..nul]).into_owned()
} else {
String::new()
}
}
"RANDOM" => {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(0);
let pid = std::process::id() as u64;
let r = (nanos.wrapping_mul(2654435761).wrapping_add(pid)) as u32;
((r as u16) & 0x7fff).to_string()
}
"SECONDS" => {
crate::ported::params::getsparam("SECONDS").unwrap_or_else(|| {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let start = self
.scalar("__zshrs_start_secs")
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(now);
now.saturating_sub(start).to_string()
})
}
"EPOCHSECONDS" => {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".to_string())
}
"EPOCHREALTIME" => {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => format!("{}.{:06}", d.as_secs(), d.subsec_micros()),
Err(_) => "0.000000".to_string(),
}
}
"argv" => self.pparams().join(" "),
"HISTCMD" => {
self.session_history_ids.len().to_string()
}
"TTY" => {
let ptr = unsafe { libc::ttyname(0) };
if ptr.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned()
}
}
"TTYIDLE" => {
let ptr = unsafe { libc::ttyname(0) };
if ptr.is_null() {
return "-1".to_string();
}
let path = unsafe { std::ffi::CStr::from_ptr(ptr) };
let path_str = path.to_string_lossy().into_owned();
match std::fs::metadata(&path_str) {
Ok(meta) => {
if let Ok(atime) = meta.accessed() {
let now = SystemTime::now();
let idle = now.duration_since(atime).unwrap_or_default();
return idle.as_secs().to_string();
}
"0".to_string()
}
Err(_) => "-1".to_string(),
}
}
"TRY_BLOCK_ERROR" => {
self.scalar("TRY_BLOCK_ERROR")
.unwrap_or_else(|| "0".to_string())
}
"patchars" => "*?[]<>(){}|^&;".to_string(),
"RANDOM_FILE" => {
if std::path::Path::new("/dev/urandom").exists() {
"/dev/urandom".to_string()
} else {
String::new()
}
}
"LINENO" => {
self.scalar("LINENO")
.unwrap_or_else(|| "1".to_string())
}
"0" => self
.scalar("0")
.unwrap_or_else(|| env::args().next().unwrap_or_default()),
n if !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) => {
let idx: usize = n.parse().unwrap_or(0);
if idx == 0 {
env::args().next().unwrap_or_default()
} else {
self.pparams()
.get(idx - 1)
.cloned()
.unwrap_or_default()
}
}
_ => {
let assoc_has_entries = self
.assoc(name)
.map(|h| !h.is_empty())
.unwrap_or(false);
let resolved = lookup_special_var(name)
.or_else(|| {
if !assoc_has_entries {
crate::ported::params::getsparam(name)
} else {
None
}
})
.or_else(|| self.array(name).map(|a| a.join(" ")))
.or_else(|| {
self.assoc(name).map(|h| {
if h.is_empty() {
String::new()
} else {
h.values().cloned().collect::<Vec<_>>().join(" ")
}
})
})
.or_else(|| env::var(name).ok());
match resolved {
Some(v) => v,
None => {
let nounset_on = crate::ported::options::opt_state_get("nounset").unwrap_or(false)
|| !crate::ported::options::opt_state_get("unset").unwrap_or(true);
if nounset_on {
zerr(&format!("{}: parameter not set", name));
std::process::exit(1);
}
String::new()
}
}
}
}
}
pub(crate) fn pre_resolve_array_subscripts(&self, expr: &str) -> String {
let bytes: Vec<char> = expr.chars().collect();
let mut out = String::with_capacity(expr.len());
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == '$' && i + 1 < bytes.len() {
let next = bytes[i + 1];
let is_special_at = next == '@' || next == '*';
let is_ident_start = next.is_ascii_alphabetic() || next == '_';
if (is_special_at || is_ident_start) && i + 2 < bytes.len() {
let mut probe = i + 1;
if is_special_at {
probe += 1;
} else {
while probe < bytes.len()
&& (bytes[probe].is_ascii_alphanumeric() || bytes[probe] == '_')
{
probe += 1;
}
}
if probe < bytes.len() && bytes[probe] == '[' {
i += 1;
continue;
}
}
}
if c.is_ascii_alphabetic() || c == '_' || c == '@' || c == '*' {
let start = i;
i += 1;
if !(bytes[start] == '@' || bytes[start] == '*') {
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == '_') {
i += 1;
}
}
let name: String = bytes[start..i].iter().collect();
if i < bytes.len() && bytes[i] == '[' {
i += 1;
let key_start = i;
let mut depth = 1;
while i < bytes.len() && depth > 0 {
match bytes[i] {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
let key_str: String = bytes[key_start..i].iter().collect();
if i < bytes.len() {
i += 1; }
let key_resolved: String = if key_str.starts_with('"') && key_str.ends_with('"')
|| key_str.starts_with('\'') && key_str.ends_with('\'')
{
key_str[1..key_str.len() - 1].to_string()
} else {
key_str.clone()
};
let trimmed_key = key_resolved.trim_start();
let resolved = if trimmed_key.starts_with('(') {
let scalar_val = crate::ported::params::getsparam(&name);
let result = if let Some(assoc) = self.assoc(&name) {
getarg(trimmed_key, None, Some(&assoc), None)
} else if name == "@" || name == "*" {
let pos = self.pparams();
getarg(trimmed_key, Some(&pos), None, None)
} else if let Some(arr) = self.array(&name) {
getarg(trimmed_key, Some(&arr), None, None)
} else if let Some(ref s) = scalar_val {
getarg(trimmed_key, None, None, Some(s.as_str()))
} else {
None
};
match result {
Some(getarg_out::Value(v)) => v.to_str(),
_ => "0".to_string(),
}
} else if let Some(assoc) = self.assoc(&name) {
assoc
.get(&key_resolved)
.cloned()
.unwrap_or_else(|| "0".to_string())
} else if let Some(arr) = self.array(&name) {
if let Ok(idx) = key_resolved.trim().parse::<i64>() {
let len = arr.len() as i64;
let pos = if idx < 0 { len + idx } else { idx - 1 };
if pos >= 0 && (pos as usize) < arr.len() {
arr[pos as usize].clone()
} else {
"0".to_string()
}
} else {
"0".to_string()
}
} else {
format!("{}[{}]", name, key_str)
};
out.push_str(&resolved);
} else {
out.push_str(&name);
}
continue;
}
out.push(c);
i += 1;
}
out
}
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn history_split_words(cmd: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let mut in_sq = false;
let mut in_dq = false;
let mut escaped = false;
for c in cmd.chars() {
if escaped {
current.push(c);
escaped = false;
continue;
}
if c == '\\' {
current.push(c);
escaped = true;
continue;
}
if c == '\'' && !in_dq {
in_sq = !in_sq;
current.push(c);
continue;
}
if c == '"' && !in_sq {
in_dq = !in_dq;
current.push(c);
continue;
}
if c.is_whitespace() && !in_sq && !in_dq {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
current.push(c);
}
if !current.is_empty() {
words.push(current);
}
words
}
pub(crate) fn history_parse_word_range(
&self,
chars: &[char],
mut i: usize,
argc: usize,
) -> (Option<usize>, Option<usize>, usize) {
if i >= chars.len() {
return (None, None, i);
}
match chars[i] {
'h' | 't' | 'r' | 'e' | 's' | 'S' | 'g' | 'p' | 'q' | 'Q' | 'l' | 'u' | 'a' | 'A'
| '&' => {
return (None, None, i - 1); }
_ => {}
}
let farg = if chars[i] == '^' {
i += 1;
Some(1usize)
} else if chars[i] == '$' {
i += 1;
return (Some(argc), Some(argc), i);
} else if chars[i] == '*' {
i += 1;
return (Some(1), Some(argc), i);
} else if chars[i].is_ascii_digit() {
let start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let n: usize = chars[start..i]
.iter()
.collect::<String>()
.parse()
.unwrap_or(0);
Some(n)
} else {
None
};
if i < chars.len() && chars[i] == '-' {
i += 1;
if i < chars.len() && chars[i] == '$' {
i += 1;
return (farg, Some(argc), i);
} else if i < chars.len() && chars[i].is_ascii_digit() {
let start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let m: usize = chars[start..i]
.iter()
.collect::<String>()
.parse()
.unwrap_or(0);
return (farg, Some(m), i);
} else {
return (farg, Some(argc.saturating_sub(1)), i);
}
}
if farg.is_some() {
(farg, farg, i)
} else {
(None, None, i)
}
}
}
impl crate::ported::exec::ShellExecutor {
pub fn run_trap(&mut self, signal: &str) {
if let Some(action) = self.traps.get(signal).cloned() {
if !action.is_empty() {
let _ = self.execute_script(&action);
}
}
}
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn expand_prompt_string(&self, s: &str) -> String {
expand_prompt(s)
}
pub(crate) fn apply_prompt_theme(&mut self, theme: &str, preview: bool) {
let (ps1, rps1) = match theme {
"minimal" => ("%# ", ""),
"off" => ("$ ", ""),
"adam1" => (
"%B%F{cyan}%n@%m %F{blue}%~%f%b %# ",
"%F{yellow}%D{%H:%M}%f",
),
"redhat" => ("[%n@%m %~]$ ", ""),
_ => ("%n@%m %~ %# ", ""),
};
if preview {
println!("PS1={:?}", ps1);
println!("RPS1={:?}", rps1);
} else {
self.set_scalar("PS1".to_string(), ps1.to_string());
self.set_scalar("RPS1".to_string(), rps1.to_string());
self.set_scalar("prompt_theme".to_string(), theme.to_string());
}
}
}
impl crate::ported::exec::ShellExecutor {
pub fn expand_glob(&self, pattern: &str) -> Vec<String> {
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(self.expand_glob(&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;
}
}
let extglob_on = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
if extglob_on {
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('.') {
continue;
}
if !crate::exec::glob_match_static(&name, neg) {
let path = if prefix.is_empty() {
name
} else {
format!("{}{}", prefix, name)
};
out.push(path);
}
}
}
out.sort();
if !out.is_empty() {
return out;
}
let nullglob = crate::ported::options::opt_state_get("nullglob").unwrap_or(false);
if nullglob {
return Vec::new();
}
let nomatch = crate::ported::options::opt_state_get("nomatch").unwrap_or(true);
if nomatch {
zerr(&format!("no matches found: {}", pattern));
std::process::exit(1);
}
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 = self.expand_glob(&lhs);
let filtered: Vec<String> = lhs_matches
.into_iter()
.filter(|p| {
let basename = p.rsplit('/').next().unwrap_or(p);
!crate::exec::glob_match_static(basename, &rhs)
&& !crate::exec::glob_match_static(p, &rhs)
})
.collect();
if !filtered.is_empty() {
return filtered;
}
let nullglob = crate::ported::options::opt_state_get("nullglob").unwrap_or(false);
if nullglob {
return Vec::new();
}
let nomatch = crate::ported::options::opt_state_get("nomatch").unwrap_or(true);
if nomatch && Self::looks_like_glob(pattern) {
zerr(&format!("no matches found: {}", pattern));
std::process::exit(1);
}
return vec![pattern.to_string()];
}
}
let (glob_pattern, qualifiers) = self.parse_glob_qualifiers(pattern);
let glob_pattern = if glob_pattern.contains("[^") {
let mut out = String::with_capacity(glob_pattern.len());
let mut chars = glob_pattern.chars().peekable();
while let Some(c) = chars.next() {
if c == '[' {
out.push('[');
if chars.peek() == Some(&'^') {
chars.next();
out.push('!');
}
for cc in chars.by_ref() {
out.push(cc);
if cc == ']' {
break;
}
}
} else {
out.push(c);
}
}
out
} else {
glob_pattern
};
let glob_pattern = if glob_pattern.contains("[:") {
let s = &glob_pattern;
let mut out = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '[' && i + 2 < chars.len() && chars[i + 1] == ':' {
let mut j = i + 2;
while j + 1 < chars.len() && !(chars[j] == ':' && chars[j + 1] == ']') {
j += 1;
}
if j + 1 < chars.len() && chars[j] == ':' && chars[j + 1] == ']' {
let name: String = chars[i + 2..j].iter().collect();
let range = match name.as_str() {
"alpha" => "a-zA-Z",
"alnum" => "a-zA-Z0-9",
"digit" => "0-9",
"xdigit" => "0-9a-fA-F",
"lower" => "a-z",
"upper" => "A-Z",
"space" => " \\t\\n\\r\\v\\f",
"blank" => " \\t",
"cntrl" => "\\x00-\\x1f\\x7f",
"print" => "\\x20-\\x7e",
"graph" => "\\x21-\\x7e",
"punct" => "!-/:-@\\[-`{-~",
_ => "",
};
if !range.is_empty() {
out.push_str(range);
i = j + 2;
continue;
}
}
}
out.push(chars[i]);
i += 1;
}
out
} else {
glob_pattern
};
let numeric_ranges = if glob_pattern.contains('<') {
extract_numeric_ranges(&glob_pattern)
} else {
Vec::new()
};
let glob_pattern = if !numeric_ranges.is_empty() {
numeric_ranges_to_star(&glob_pattern)
} else {
glob_pattern
};
if self.has_extglob_pattern(&glob_pattern) {
let expanded = self.expand_glob(&glob_pattern);
return self.filter_by_qualifiers(expanded, &qualifiers);
}
let nullglob = crate::ported::options::opt_state_get("nullglob").unwrap_or(false);
let last_seg = glob_pattern.rsplit('/').next().unwrap_or(&glob_pattern);
let pattern_starts_with_dot = last_seg.starts_with('.');
let dotglob = crate::ported::options::opt_state_get("dotglob").unwrap_or(false)
|| crate::ported::options::opt_state_get("globdots").unwrap_or(false)
|| qualifiers.contains('D')
|| pattern_starts_with_dot;
let nocaseglob = !crate::ported::options::opt_state_get("caseglob").unwrap_or(true)
|| crate::ported::options::opt_state_get("nocaseglob").unwrap_or(false);
let mut expanded = if !numeric_ranges.is_empty() {
self.expand_glob_with_numeric_range(pattern, &numeric_ranges, dotglob, nocaseglob)
} else if glob_pattern.contains("**/") {
self.expand_glob_parallel(&glob_pattern, dotglob, nocaseglob)
} else {
let options = glob::MatchOptions {
case_sensitive: !nocaseglob,
require_literal_separator: false,
require_literal_leading_dot: !dotglob,
};
match glob::glob_with(&glob_pattern, options) {
Ok(paths) => paths
.filter_map(|p| p.ok())
.map(|p| p.to_string_lossy().to_string())
.collect(),
Err(_) => vec![],
}
};
expanded.retain(|p| {
let last = p.rsplit('/').next().unwrap_or(p);
last != "." && last != ".."
});
let expanded = self.filter_by_qualifiers(expanded, &qualifiers);
let mut expanded = expanded;
if glob_pattern.ends_with('/') {
for p in expanded.iter_mut() {
if !p.ends_with('/') {
p.push('/');
}
}
}
let user_sort = qualifiers.contains('o') || qualifiers.contains('O');
if !user_sort {
if glob_pattern.contains("**/") {
expanded.sort_by(|a, b| crate::ported::sort::zstrcmp(a, b, 0));
} else {
expanded.sort_by(|a, b| {
let an = a.rsplit('/').next().unwrap_or(a);
let bn = b.rsplit('/').next().unwrap_or(b);
crate::ported::sort::zstrcmp(an, bn, 0)
});
}
}
if expanded.is_empty() {
if nullglob || qualifiers.contains('N') {
return vec![];
}
let nomatch = crate::ported::options::opt_state_get("nomatch").unwrap_or(true);
if nomatch && Self::looks_like_glob(pattern) {
zerr(&format!("no matches found: {}", pattern));
self.current_command_glob_failed.set(true);
return Vec::new();
}
vec![pattern.to_string()]
} else {
expanded
}
}
pub(crate) fn looks_like_glob(pattern: &str) -> bool {
let has_qual_suffix = if let Some(open) = pattern.rfind('(') {
pattern.ends_with(')') && open + 1 < pattern.len() - 1
} else {
false
};
let body = if let Some(open) = pattern.rfind('(') {
if pattern.ends_with(')') {
&pattern[..open]
} else {
pattern
}
} else {
pattern
};
let chars: Vec<char> = body.chars().collect();
let mut i = 0;
let mut has_unescaped_star = false;
let mut has_unescaped_question = false;
let mut has_unescaped_bracket_open: Option<usize> = None;
while i < chars.len() {
let c = chars[i];
if c == '\\' && i + 1 < chars.len() {
i += 2;
continue;
}
match c {
'*' => has_unescaped_star = true,
'?' => has_unescaped_question = true,
'[' if has_unescaped_bracket_open.is_none() => {
has_unescaped_bracket_open = Some(i);
}
_ => {}
}
i += 1;
}
let has_bracket_class = has_unescaped_bracket_open
.map(|i| body[i + 1..].contains(']'))
.unwrap_or(false);
let has_numeric_range =
body.contains('<') && body.contains('>') && !extract_numeric_ranges(body).is_empty();
has_unescaped_star
|| has_unescaped_question
|| has_bracket_class
|| has_qual_suffix
|| has_numeric_range
}
pub(crate) fn expand_glob_with_numeric_range(
&self,
pattern: &str,
ranges: &[(usize, usize, Option<i64>, Option<i64>)],
dotglob: bool,
nocaseglob: bool,
) -> Vec<String> {
let (dir_part, file_part) = match pattern.rfind('/') {
Some(idx) => (&pattern[..idx], &pattern[idx + 1..]),
None => ("", pattern),
};
let mut rx = String::from("^");
let chars: Vec<char> = file_part.chars().collect();
let mut i = 0;
let mut in_bracket = false;
while i < chars.len() {
let c = chars[i];
if c == '[' && !in_bracket {
in_bracket = true;
rx.push('[');
i += 1;
continue;
}
if c == ']' && in_bracket {
in_bracket = false;
rx.push(']');
i += 1;
continue;
}
if in_bracket {
rx.push(c);
i += 1;
continue;
}
if c == '<' {
let mut j = i + 1;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
if j < chars.len() && chars[j] == '-' {
j += 1;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
if j < chars.len() && chars[j] == '>' {
rx.push_str("(\\d+)");
i = j + 1;
continue;
}
}
}
match c {
'*' => rx.push_str(".*"),
'?' => rx.push('.'),
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '\\' | '{' | '}' => {
rx.push('\\');
rx.push(c);
}
_ => rx.push(c),
}
i += 1;
}
rx.push('$');
let re = match if nocaseglob {
regex::RegexBuilder::new(&rx).case_insensitive(true).build()
} else {
regex::Regex::new(&rx).map_err(|e| regex::Error::Syntax(e.to_string()))
} {
Ok(r) => r,
Err(_) => return Vec::new(),
};
let mut dirs: Vec<String> = if dir_part.is_empty() {
vec![".".to_string()]
} else if dir_part.contains('*')
|| dir_part.contains('?')
|| dir_part.contains('[')
|| dir_part.contains('<')
{
let opts = glob::MatchOptions {
case_sensitive: !nocaseglob,
require_literal_separator: false,
require_literal_leading_dot: !dotglob,
};
match glob::glob_with(dir_part, opts) {
Ok(paths) => paths
.filter_map(|p| p.ok())
.filter(|p| p.is_dir())
.map(|p| p.to_string_lossy().to_string())
.collect(),
Err(_) => return Vec::new(),
}
} else {
vec![dir_part.to_string()]
};
if dirs.is_empty() {
dirs.push(dir_part.to_string());
}
let mut out = Vec::new();
for dir in &dirs {
let read = match std::fs::read_dir(if dir.is_empty() { "." } else { dir }) {
Ok(r) => r,
Err(_) => continue,
};
for entry in read.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !dotglob && name.starts_with('.') && !file_part.starts_with('.') {
continue;
}
let caps = match re.captures(&name) {
Some(c) => c,
None => continue,
};
let mut ok = true;
for (idx, range) in ranges.iter().enumerate() {
let cap = match caps.get(idx + 1) {
Some(m) => m.as_str(),
None => {
ok = false;
break;
}
};
let val: i64 = match cap.parse() {
Ok(v) => v,
Err(_) => {
ok = false;
break;
}
};
if !numeric_range_contains(range.2, range.3, val) {
ok = false;
break;
}
}
if !ok {
continue;
}
let full = if dir == "." || dir.is_empty() {
name
} else if dir.ends_with('/') {
format!("{}{}", dir, name)
} else {
format!("{}/{}", dir, name)
};
out.push(full);
}
}
out.sort();
out
}
pub(crate) fn expand_glob_parallel(&self, pattern: &str, dotglob: bool, nocaseglob: bool) -> Vec<String> {
let (base, file_glob) = if let Some(pos) = pattern.find("**/") {
let base = if pos == 0 {
"."
} else {
&pattern[..pos.saturating_sub(1)]
};
let rest = &pattern[pos + 3..]; (base.to_string(), rest.to_string())
} else {
return vec![];
};
let dirs_only = file_glob.is_empty();
if file_glob.contains("**/") {
let options = glob::MatchOptions {
case_sensitive: !nocaseglob,
require_literal_separator: false,
require_literal_leading_dot: !dotglob,
};
return match glob::glob_with(pattern, options) {
Ok(paths) => paths
.filter_map(|p| p.ok())
.map(|p| p.to_string_lossy().to_string())
.collect(),
Err(_) => vec![],
};
}
let match_opts = glob::MatchOptions {
case_sensitive: !nocaseglob,
require_literal_separator: false,
require_literal_leading_dot: !dotglob,
};
let file_pat = if dirs_only {
None
} else {
match glob::Pattern::new(&file_glob) {
Ok(p) => Some(p),
Err(_) => return vec![],
}
};
let match_dirs_too = !dirs_only;
let top_entries: Vec<std::path::PathBuf> = match std::fs::read_dir(&base) {
Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.path()).collect(),
Err(_) => return vec![],
};
let mut results: Vec<String> = Vec::new();
for entry in &top_entries {
let is_dir = entry.is_dir();
let is_file = entry.is_file() || entry.is_symlink();
let want = if dirs_only {
is_dir
} else {
is_file || (match_dirs_too && is_dir)
};
if want {
if let Some(name) = entry.file_name().and_then(|n| n.to_str()) {
let matches = match &file_pat {
None => true,
Some(p) => p.matches_with(name, match_opts),
};
if matches {
let mut s = entry.to_string_lossy().to_string();
if dirs_only {
s.push('/');
}
results.push(s);
}
}
}
}
let subdirs: Vec<std::path::PathBuf> = top_entries
.into_iter()
.filter(|p| p.is_dir())
.filter(|p| {
dotglob
|| !p
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with('.'))
.unwrap_or(false)
})
.collect();
if subdirs.is_empty() {
return results;
}
let (tx, rx) = std::sync::mpsc::channel::<Vec<String>>();
for subdir in &subdirs {
let tx = tx.clone();
let subdir = subdir.clone();
let file_pat = file_pat.clone();
let skip_dot = !dotglob;
let dirs_only_w = dirs_only;
let match_dirs_too_w = match_dirs_too;
self.worker_pool.submit(move || {
let mut matches = Vec::new();
let walker = WalkDir::new(&subdir)
.follow_links(false)
.into_iter()
.filter_entry(move |e| {
if skip_dot {
if let Some(name) = e.file_name().to_str() {
if name.starts_with('.') && e.depth() > 0 {
return false;
}
}
}
true
});
for entry in walker.filter_map(|e| e.ok()) {
let is_file = entry.file_type().is_file() || entry.file_type().is_symlink();
let is_dir = entry.file_type().is_dir();
if entry.depth() == 0 {
continue;
}
let want = if dirs_only_w {
is_dir
} else {
is_file || (match_dirs_too_w && is_dir)
};
if want {
if let Some(name) = entry.file_name().to_str() {
let matches_pat = match &file_pat {
None => true,
Some(p) => p.matches_with(name, match_opts),
};
if matches_pat {
let mut s = entry.path().to_string_lossy().to_string();
if dirs_only_w {
s.push('/');
}
matches.push(s);
}
}
}
}
let _ = tx.send(matches);
});
}
drop(tx);
for batch in rx {
results.extend(batch);
}
if base == "." {
results = results
.into_iter()
.map(|s| s.strip_prefix("./").map(|t| t.to_string()).unwrap_or(s))
.collect();
}
results.sort();
results
}
pub(crate) fn parse_glob_qualifiers(&self, pattern: &str) -> (String, String) {
if !pattern.ends_with(')') {
return (pattern.to_string(), String::new());
}
let chars: Vec<char> = pattern.chars().collect();
let mut depth = 0;
let mut qual_start = None;
for i in (0..chars.len()).rev() {
match chars[i] {
')' => depth += 1,
'(' => {
depth -= 1;
if depth == 0 {
qual_start = Some(i);
break;
}
}
_ => {}
}
}
if let Some(start) = qual_start {
let qual_content: String = chars[start + 1..chars.len() - 1].iter().collect();
if !qual_content.contains('|') && self.looks_like_glob_qualifiers(&qual_content) {
let base_pattern: String = chars[..start].iter().collect();
return (base_pattern, qual_content);
}
}
(pattern.to_string(), String::new())
}
pub(crate) fn looks_like_glob_qualifiers(&self, s: &str) -> bool {
if s.is_empty() {
return false;
}
let valid_chars = "./@=p*%bghilrwxAIERWXsStfHedDLNnMmcaouUYHTk^-+:0123456789,[]FO";
s.chars()
.all(|c| valid_chars.contains(c) || c.is_whitespace())
}
pub(crate) fn filter_by_qualifiers(&self, files: Vec<String>, qualifiers: &str) -> Vec<String> {
if qualifiers.is_empty() {
return files;
}
let has_or = {
let mut depth_b = 0;
let mut depth_p = 0;
let mut found = false;
for c in qualifiers.chars() {
match c {
'[' => depth_b += 1,
']' if depth_b > 0 => depth_b -= 1,
'(' if depth_b == 0 => depth_p += 1,
')' if depth_b == 0 && depth_p > 0 => depth_p -= 1,
',' if depth_b == 0 && depth_p == 0 => {
found = true;
break;
}
_ => {}
}
}
found
};
if has_or {
let mut clauses: Vec<String> = Vec::new();
let mut current = String::new();
let mut depth_b = 0;
let mut depth_p = 0;
for c in qualifiers.chars() {
match c {
'[' => {
depth_b += 1;
current.push(c);
}
']' if depth_b > 0 => {
depth_b -= 1;
current.push(c);
}
'(' if depth_b == 0 => {
depth_p += 1;
current.push(c);
}
')' if depth_b == 0 && depth_p > 0 => {
depth_p -= 1;
current.push(c);
}
',' if depth_b == 0 && depth_p == 0 => {
clauses.push(std::mem::take(&mut current));
}
_ => current.push(c),
}
}
clauses.push(current);
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut out: Vec<String> = Vec::new();
for clause in &clauses {
let matched = self.filter_by_qualifiers(files.clone(), clause);
for m in matched {
if seen.insert(m.clone()) {
out.push(m);
}
}
}
return out;
}
let meta_cache = self.prefetch_metadata(&files);
let mut result = files;
let mut negate = false;
let mut mark_dirs = false;
let mut list_types = false;
let mut chars = qualifiers.chars().peekable();
while let Some(c) = chars.next() {
match c {
'^' => negate = !negate,
'M' => {
mark_dirs = !negate;
negate = false;
}
'T' => {
list_types = !negate;
negate = false;
}
':' => {
let mut mods = String::from(":");
while chars.peek().is_some() {
mods.push(chars.next().unwrap());
}
let modref = mods.as_str();
result = result
.into_iter()
.map(|p| crate::ported::hist::apply_history_modifiers(&p, modref))
.collect();
}
'.' => {
let follow_links = qualifiers.contains('-');
result.retain(|f| {
let is_plain_file = meta_cache
.get(f)
.map(|(m, sm)| {
let is_link = sm
.as_ref()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
let is_reg = m.as_ref().map(|m| m.is_file()).unwrap_or(false);
if follow_links {
is_reg
} else {
is_reg && !is_link
}
})
.unwrap_or(false);
if negate {
!is_plain_file
} else {
is_plain_file
}
});
negate = false;
}
'/' => {
result.retain(|f| {
let is_dir = meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.is_dir())
.unwrap_or(false);
if negate {
!is_dir
} else {
is_dir
}
});
negate = false;
}
'@' => {
result.retain(|f| {
let is_link = meta_cache
.get(f)
.and_then(|(_, sm)| sm.as_ref())
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
if negate {
!is_link
} else {
is_link
}
});
negate = false;
}
'=' => {
result.retain(|f| {
let is_socket = meta_cache
.get(f)
.and_then(|(_, sm)| sm.as_ref())
.map(|m| m.file_type().is_socket())
.unwrap_or(false);
if negate {
!is_socket
} else {
is_socket
}
});
negate = false;
}
'p' => {
result.retain(|f| {
let is_fifo = meta_cache
.get(f)
.and_then(|(_, sm)| sm.as_ref())
.map(|m| m.file_type().is_fifo())
.unwrap_or(false);
if negate {
!is_fifo
} else {
is_fifo
}
});
negate = false;
}
'*' => {
result.retain(|f| {
let is_exec = meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
.unwrap_or(false);
if negate {
!is_exec
} else {
is_exec
}
});
negate = false;
}
'%' => {
let next = chars.peek().copied();
result.retain(|f| {
let is_device = meta_cache
.get(f)
.and_then(|(_, sm)| sm.as_ref())
.map(|m| match next {
Some('b') => m.file_type().is_block_device(),
Some('c') => m.file_type().is_char_device(),
_ => {
m.file_type().is_block_device()
|| m.file_type().is_char_device()
}
})
.unwrap_or(false);
if negate {
!is_device
} else {
is_device
}
});
if next == Some('b') || next == Some('c') {
chars.next();
}
negate = false;
}
'L' => {
let mut cmp = '=';
if let Some(&peek) = chars.peek() {
if peek == '+' || peek == '-' {
cmp = peek;
chars.next();
}
}
let mut num_str = String::new();
while let Some(&peek) = chars.peek() {
if peek.is_ascii_digit() {
num_str.push(peek);
chars.next();
} else {
break;
}
}
let n: u64 = num_str.parse().unwrap_or(0);
let unit_mult: u64 = match chars.peek().copied() {
Some('k') | Some('K') => {
chars.next();
1024
}
Some('m') | Some('M') => {
chars.next();
1024 * 1024
}
Some('g') | Some('G') => {
chars.next();
1024 * 1024 * 1024
}
Some('p') | Some('P') => {
chars.next();
1
}
_ => 1,
};
let target = n * unit_mult;
result.retain(|f| {
let size = meta_cache
.get(f)
.map(|(m, sm)| {
sm.as_ref()
.map(|m| m.len())
.unwrap_or_else(|| m.as_ref().map(|m| m.len()).unwrap_or(0))
})
.unwrap_or(0);
let pass = match cmp {
'+' => size > target,
'-' => size < target,
_ => size == target,
};
if negate {
!pass
} else {
pass
}
});
negate = false;
}
'l' => {
let mut cmp = '=';
if let Some(&peek) = chars.peek() {
if peek == '+' || peek == '-' {
cmp = peek;
chars.next();
}
}
let mut num_str = String::new();
while let Some(&peek) = chars.peek() {
if peek.is_ascii_digit() {
num_str.push(peek);
chars.next();
} else {
break;
}
}
let target: u64 = num_str.parse().unwrap_or(0);
result.retain(|f| {
let nlink = meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.nlink())
.unwrap_or(0);
let matches = match cmp {
'+' => nlink > target,
'-' => nlink < target,
_ => nlink == target,
};
if negate {
!matches
} else {
matches
}
});
negate = false;
}
'r' => {
result = self.filter_by_permission(result, 0o400, negate, &meta_cache);
negate = false;
}
'w' => {
result = self.filter_by_permission(result, 0o200, negate, &meta_cache);
negate = false;
}
'x' => {
result = self.filter_by_permission(result, 0o100, negate, &meta_cache);
negate = false;
}
'A' => {
result = self.filter_by_permission(result, 0o040, negate, &meta_cache);
negate = false;
}
'I' => {
result = self.filter_by_permission(result, 0o020, negate, &meta_cache);
negate = false;
}
'E' => {
result = self.filter_by_permission(result, 0o010, negate, &meta_cache);
negate = false;
}
'R' => {
result = self.filter_by_permission(result, 0o004, negate, &meta_cache);
negate = false;
}
'W' => {
result = self.filter_by_permission(result, 0o002, negate, &meta_cache);
negate = false;
}
'X' => {
result = self.filter_by_permission(result, 0o001, negate, &meta_cache);
negate = false;
}
's' => {
result = self.filter_by_permission(result, 0o4000, negate, &meta_cache);
negate = false;
}
'S' => {
result = self.filter_by_permission(result, 0o2000, negate, &meta_cache);
negate = false;
}
't' => {
result = self.filter_by_permission(result, 0o1000, negate, &meta_cache);
negate = false;
}
'F' => {
result.retain(|f| {
let path = std::path::Path::new(f);
let is_nonempty = path.is_dir()
&& std::fs::read_dir(path)
.map(|mut d| d.next().is_some())
.unwrap_or(false);
if negate {
!is_nonempty
} else {
is_nonempty
}
});
negate = false;
}
'U' => {
let euid = unsafe { libc::geteuid() };
result.retain(|f| {
let is_owned = meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.uid() == euid)
.unwrap_or(false);
if negate {
!is_owned
} else {
is_owned
}
});
negate = false;
}
'G' => {
let egid = unsafe { libc::getegid() };
result.retain(|f| {
let is_owned = meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.gid() == egid)
.unwrap_or(false);
if negate {
!is_owned
} else {
is_owned
}
});
negate = false;
}
'o' => {
if chars.peek() == Some(&'n') {
chars.next();
result.sort();
} else if chars.peek() == Some(&'L') {
chars.next();
result.sort_by_key(|f| {
meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.len())
.unwrap_or(0)
});
} else if chars.peek() == Some(&'m') {
chars.next();
result.sort_by_key(|f| {
meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.and_then(|m| m.modified().ok())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
result.reverse();
} else if chars.peek() == Some(&'a') {
chars.next();
result.sort_by_key(|f| {
meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.and_then(|m| m.accessed().ok())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
result.reverse();
} else if chars.peek() == Some(&'c') {
chars.next();
result.sort_by_key(|f| {
meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| {
std::time::UNIX_EPOCH
+ std::time::Duration::from_secs(m.ctime() as u64)
})
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
result.reverse();
}
}
'O' => {
if chars.peek() == Some(&'n') {
chars.next();
result.sort();
result.reverse();
} else if chars.peek() == Some(&'L') {
chars.next();
result.sort_by_key(|f| {
meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| m.len())
.unwrap_or(0)
});
result.reverse();
} else if chars.peek() == Some(&'m') {
chars.next();
result.sort_by_key(|f| {
meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.and_then(|m| m.modified().ok())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
} else {
result.reverse();
}
}
'[' => {
let mut range_str = String::new();
while let Some(&ch) = chars.peek() {
if ch == ']' {
chars.next();
break;
}
range_str.push(chars.next().unwrap());
}
if let Some((start, end)) = self.parse_subscript_range(&range_str, result.len())
{
result = result.into_iter().skip(start).take(end - start).collect();
}
}
'D' => {
}
'N' => {
}
'm' | 'a' | 'c' => {
let qual_kind = c;
let unit_secs: i64 = match chars.peek().copied() {
Some('s') => {
chars.next();
1
}
Some('m') => {
chars.next();
60
}
Some('h') => {
chars.next();
3600
}
Some('d') => {
chars.next();
86400
}
Some('w') => {
chars.next();
7 * 86400
}
Some('M') => {
chars.next();
30 * 86400
}
_ => 86400,
};
let op = match chars.peek().copied() {
Some('+') => {
chars.next();
'+'
}
Some('-') => {
chars.next();
'-'
}
_ => '=',
};
let mut nstr = String::new();
while let Some(&nc) = chars.peek() {
if nc.is_ascii_digit() {
nstr.push(nc);
chars.next();
} else {
break;
}
}
let n: i64 = nstr.parse().unwrap_or(0);
let cutoff = n * unit_secs;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
result.retain(|f| {
let m = match meta_cache.get(f).and_then(|(m, _)| m.as_ref()) {
Some(m) => m,
None => return false,
};
let ts = match qual_kind {
'm' => m.mtime(),
'a' => m.atime(),
'c' => m.ctime(),
_ => 0,
};
let age = now - ts;
let pass = match op {
'+' => age > cutoff,
'-' => age < cutoff,
_ => age >= cutoff && age < cutoff + unit_secs,
};
if negate {
!pass
} else {
pass
}
});
negate = false;
}
_ => {}
}
}
if mark_dirs || list_types {
result = result
.into_iter()
.map(|p| {
let meta = match std::fs::symlink_metadata(&p) {
Ok(m) => m,
Err(_) => return p,
};
let ch = crate::glob::file_type(meta.permissions().mode());
if list_types || (mark_dirs && ch == '/') {
format!("{}{}", p, ch)
} else {
p
}
})
.collect();
}
result
}
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn prefetch_metadata(
&self,
files: &[String],
) -> HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)> {
let in_forked_child = {
static MAIN_PID: AtomicI32 = AtomicI32::new(0);
let mut main = MAIN_PID.load(Ordering::Relaxed);
if main == 0 {
let cur = nix::unistd::getpid().as_raw();
match MAIN_PID.compare_exchange(0, cur, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => main = cur,
Err(prev) => main = prev,
}
}
nix::unistd::getpid().as_raw() != main
};
if files.len() < 32 || in_forked_child {
return files
.iter()
.map(|f| {
let meta = std::fs::metadata(f).ok();
let symlink_meta = std::fs::symlink_metadata(f).ok();
(f.clone(), (meta, symlink_meta))
})
.collect();
}
let pool_size = self.worker_pool.size();
let chunk_size = files.len().div_ceil(pool_size);
let (tx, rx) = std::sync::mpsc::channel();
for chunk in files.chunks(chunk_size) {
let tx = tx.clone();
let chunk: Vec<String> = chunk.to_vec();
self.worker_pool.submit(move || {
#[allow(clippy::type_complexity)]
let batch: Vec<(
String,
(Option<std::fs::Metadata>, Option<std::fs::Metadata>),
)> = chunk
.into_iter()
.map(|f| {
let meta = std::fs::metadata(&f).ok();
let symlink_meta = std::fs::symlink_metadata(&f).ok();
(f, (meta, symlink_meta))
})
.collect();
let _ = tx.send(batch);
});
}
drop(tx);
let mut map = HashMap::with_capacity(files.len());
for batch in rx {
for (path, metas) in batch {
map.insert(path, metas);
}
}
map
}
pub(crate) fn filter_by_permission(
&self,
files: Vec<String>,
mode: u32,
negate: bool,
meta_cache: &HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)>,
) -> Vec<String> {
files
.into_iter()
.filter(|f| {
let has_perm = meta_cache
.get(f)
.and_then(|(m, _)| m.as_ref())
.map(|m| (m.permissions().mode() & mode) != 0)
.unwrap_or(false);
if negate {
!has_perm
} else {
has_perm
}
})
.collect()
}
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn copy_dir_recursive(src: &std::path::Path, dest: &std::path::Path) -> std::io::Result<()> {
if !dest.exists() {
std::fs::create_dir_all(dest)?;
}
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if file_type.is_dir() {
Self::copy_dir_recursive(&src_path, &dest_path)?;
} else {
std::fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn bin_cap(&self, args: &[String]) -> i32 {
let ops = crate::ported::zsh_h::options { ind: [0u8; crate::ported::zsh_h::MAX_OPS], args: Vec::new(), argscount: 0, argsalloc: 0 };
bin_cap("cap", args, &ops, 0)
}
pub(crate) fn bin_getcap(&self, args: &[String]) -> i32 {
let ops = crate::ported::zsh_h::options { ind: [0u8; crate::ported::zsh_h::MAX_OPS], args: Vec::new(), argscount: 0, argsalloc: 0 };
bin_getcap("getcap", args, &ops, 0)
}
pub(crate) fn bin_setcap(&self, args: &[String]) -> i32 {
let ops = crate::ported::zsh_h::options { ind: [0u8; crate::ported::zsh_h::MAX_OPS], args: Vec::new(), argscount: 0, argsalloc: 0 };
bin_setcap("setcap", args, &ops, 0)
}
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
pub(crate) fn bin_echoti(&mut self, args: &[String]) -> i32 {
let ops = options { ind: [0u8; MAX_OPS], args: Vec::new(),
argscount: 0, argsalloc: 0 };
crate::ported::modules::terminfo::bin_echoti("echoti", args, &ops, 0)
}
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
}
impl crate::ported::exec::ShellExecutor {
}
use std::cell::RefCell;
thread_local! {
static SCAN_KEYS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
pub fn scan_magic_assoc_keys(name: &str) -> Option<Vec<String>> {
fn collect<F: FnOnce(Option<crate::ported::zsh_h::ScanFunc>, i32)>(scan: F)
-> Vec<String>
{
SCAN_KEYS.with(|k| k.borrow_mut().clear());
fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
SCAN_KEYS.with(|k| k.borrow_mut().push(node.nam.clone()));
}
scan(Some(cb), 0);
SCAN_KEYS.with(|k| k.borrow().clone())
}
let null_ht = std::ptr::null_mut();
match name {
"commands" => Some(collect(|f, fl| scanpmcommands(null_ht, f, fl))),
"options" => Some(collect(|f, fl| scanpmoptions(null_ht, f, fl))),
"builtins" => Some(collect(|f, fl| scanpmbuiltins(null_ht, f, fl))),
"dis_builtins" => Some(collect(|f, fl| scanpmdisbuiltins(null_ht, f, fl))),
"functions" => Some(collect(|f, fl| scanpmfunctions(null_ht, f, fl))),
"dis_functions"=> Some(collect(|f, fl| scanpmdisfunctions(null_ht, f, fl))),
"aliases" => Some(collect(|f, fl| scanpmraliases(null_ht, f, fl))),
"dis_aliases" => Some(collect(|f, fl| scanpmdisraliases(null_ht, f, fl))),
"galiases" => Some(collect(|f, fl| scanpmgaliases(null_ht, f, fl))),
"dis_galiases" => Some(collect(|f, fl| scanpmdisgaliases(null_ht, f, fl))),
"saliases" => Some(collect(|f, fl| scanpmsaliases(null_ht, f, fl))),
"dis_saliases" => Some(collect(|f, fl| scanpmdissaliases(null_ht, f, fl))),
"reswords" | "dis_reswords" |
"modules" | "history" | "historywords" |
"jobtexts" | "jobstates" | "jobdirs" |
"nameddirs" | "userdirs" | "usergroups" |
"parameters" | "errnos" | "sysparams" | "dirstack"
=> Some(Vec::new()),
"mapfile" => Some(
crate::modules::mapfile::scanpmmapfile()
.into_iter()
.map(|(k, _v)| k)
.collect(),
),
_ => None,
}
}
impl crate::ported::exec::ShellExecutor {
pub fn enter_posix_mode(&mut self) {
self.posix_mode = true;
self.plugin_cache = None;
self.compsys_cache = None;
self.compinit_pending = None;
self.worker_pool = std::sync::Arc::new(crate::worker::WorkerPool::new(1));
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(), argscount: 0, argsalloc: 0,
};
crate::ported::builtin::bin_emulate("emulate",
&["sh".to_string(), "-R".to_string()], &ops, 0);
}
pub fn enter_ksh_mode(&mut self) {
self.plugin_cache = None;
self.compsys_cache = None;
self.compinit_pending = None;
self.worker_pool = std::sync::Arc::new(crate::worker::WorkerPool::new(1));
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(), argscount: 0, argsalloc: 0,
};
crate::ported::builtin::bin_emulate("emulate",
&["ksh".to_string(), "-R".to_string()], &ops, 0);
}
}
pub fn glob_match_static(s: &str, pattern: &str) -> bool {
let extendedglob_on =
with_executor(|e| crate::ported::options::opt_state_get("extendedglob").unwrap_or(false));
if extendedglob_on {
if let Some(rest) = pattern.strip_prefix('^') {
return !crate::exec::glob_match_static(s, rest);
}
if let Some(idx) = find_top_level_tilde(pattern) {
let lhs = &pattern[..idx];
let rhs = &pattern[idx + 1..];
return crate::exec::glob_match_static(s, lhs)
&& !crate::exec::glob_match_static(s, rhs);
}
}
let kshglob_on = with_executor(|e| crate::ported::options::opt_state_get("kshglob").unwrap_or(false));
if kshglob_on {
if let Some(body) = pattern.strip_prefix("!(").and_then(|r| r.strip_suffix(')')) {
let mut depth = 0;
let mut balanced = true;
for c in body.chars() {
match c {
'(' => depth += 1,
')' => {
if depth == 0 {
balanced = false;
break;
}
depth -= 1;
}
_ => {}
}
}
if balanced && depth == 0 {
return !crate::exec::glob_match_static(s, body);
}
}
}
let (pattern, case_insensitive, l_flag, approx_n) =
if let Some((bits, _assert, consumed)) =
crate::ported::pattern::patgetglobflags(pattern)
{
let ci = (bits & crate::ported::zsh_h::GF_IGNCASE) != 0;
let l = (bits & crate::ported::zsh_h::GF_LCMATCHUC) != 0;
let errs = bits & 0xff;
let approx = if errs != 0 { Some(errs as u32) } else { None };
(pattern[consumed..].to_string(), ci, l, approx)
} else {
(pattern.to_string(), false, false, None)
};
if let Some(n) = approx_n {
let s_chars: Vec<char> = s.chars().collect();
let p_chars: Vec<char> = pattern.chars().collect();
let m = s_chars.len();
let k = p_chars.len();
if m.abs_diff(k) as u32 > n {
return false;
}
let mut prev: Vec<usize> = (0..=k).collect();
let mut curr: Vec<usize> = vec![0; k + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=k {
let cost = if s_chars[i - 1] == p_chars[j - 1] { 0 } else { 1 };
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
return prev[k] as u32 <= n;
}
let mut regex_pattern = String::from("^");
let mut numeric_ranges: Vec<(usize, Option<i64>, Option<i64>)> = Vec::new();
let mut capture_group_count: usize = 0;
let mut chars = pattern.chars().peekable();
let consume_extglob_postfix =
|chars: &mut std::iter::Peekable<std::str::Chars>| -> Option<&'static str> {
if !extendedglob_on {
return None;
}
if chars.peek() != Some(&'#') {
return None;
}
chars.next();
if chars.peek() == Some(&'#') {
chars.next();
Some("+")
} else {
Some("*")
}
};
while let Some(c) = chars.next() {
match c {
'?' | '*' | '+' | '@'
if chars.peek() == Some(&'(')
&& with_executor(|e| {
crate::ported::options::opt_state_get("kshglob").unwrap_or(false)
}) =>
{
let op = c;
chars.next(); let mut depth = 1;
let mut body = String::new();
while let Some(&pc) = chars.peek() {
chars.next();
if pc == '(' {
depth += 1;
body.push(pc);
} else if pc == ')' {
depth -= 1;
if depth == 0 {
break;
}
body.push(pc);
} else {
body.push(pc);
}
}
let body_re = {
let mut out = String::new();
let mut chars = body.chars().peekable();
while let Some(c) = chars.next() {
match c {
'|' => out.push('|'),
'*' => out.push_str(".*"),
'?' => out.push('.'),
'[' => {
out.push('[');
for cc in chars.by_ref() {
if cc == ']' {
out.push(']');
break;
}
out.push(cc);
}
}
'.' | '+' | '^' | '$' | '\\' | '{' | '}' | '(' | ')' => {
out.push('\\');
out.push(c);
}
_ => out.push(c),
}
}
out
};
let suffix = match op {
'?' => "?",
'*' => "*",
'+' => "+",
'@' => "",
_ => "",
};
regex_pattern.push_str(&format!("(?:{}){}", body_re, suffix));
}
'*' => regex_pattern.push_str(".*"),
'?' => {
regex_pattern.push('.');
if let Some(q) = consume_extglob_postfix(&mut chars) {
regex_pattern.push_str(q);
}
}
'<' => {
let parsed: Option<(Option<i64>, Option<i64>, usize)> = (|| {
let mut buf = String::new();
let peek_iter = chars.clone();
for c in peek_iter {
buf.push(c);
if c == '>' { break; }
if buf.len() > 64 { return None; }
}
if !buf.ends_with('>') {
return None;
}
let inner = &buf[..buf.len() - 1];
let (lo_str, hi_str) = inner.split_once('-')?;
let lo: Option<i64> = if lo_str.is_empty() {
None
} else {
Some(lo_str.parse().ok()?)
};
let hi: Option<i64> = if hi_str.is_empty() {
None
} else {
Some(hi_str.parse().ok()?)
};
let n = buf.chars().count();
for _ in 0..n { chars.next(); }
Some((lo, hi, n))
})();
if let Some((lo, hi, consumed)) = parsed {
regex_pattern.push_str("(\\d+)");
capture_group_count += 1;
numeric_ranges.push((capture_group_count, lo, hi));
let _ = consumed;
} else {
regex_pattern.push('<');
}
}
'[' => {
regex_pattern.push('[');
let mut first = true;
while let Some(cc) = chars.next() {
if first && cc == '!' {
regex_pattern.push('^');
first = false;
continue;
}
first = false;
if cc == ']' {
regex_pattern.push(']');
break;
}
if cc == '\\' {
regex_pattern.push('\\');
if let Some(nx) = chars.next() {
regex_pattern.push(nx);
}
continue;
}
if cc == '[' && chars.peek() == Some(&':') {
regex_pattern.push('[');
let mut prev_colon = false;
for ic in chars.by_ref() {
regex_pattern.push(ic);
if prev_colon && ic == ']' {
break;
}
prev_colon = ic == ':';
}
continue;
}
regex_pattern.push(cc);
}
if let Some(q) = consume_extglob_postfix(&mut chars) {
regex_pattern.push_str(q);
}
}
'(' => {
let peek_iter = chars.clone();
let mut probe: Vec<char> = Vec::new();
let p = peek_iter;
for pc in p {
probe.push(pc);
if pc == ')' || probe.len() > 32 {
break;
}
}
let probe_str: String = probe.iter().collect();
if probe_str.starts_with("#c") && probe_str.ends_with(')') {
let body = &probe_str[2..probe_str.len() - 1];
let quant = if let Some((lo, hi)) = body.split_once(',') {
format!("{{{},{}}}", lo, hi)
} else {
format!("{{{}}}", body)
};
regex_pattern.push_str(&quant);
for _ in 0..probe.len() {
chars.next();
}
} else if probe_str == "#e)" {
regex_pattern.push('$');
for _ in 0..probe.len() {
chars.next();
}
} else if probe_str == "#s)" {
regex_pattern.push('^');
for _ in 0..probe.len() {
chars.next();
}
} else {
regex_pattern.push('(');
capture_group_count += 1;
}
}
')' => {
regex_pattern.push(')');
if let Some(q) = consume_extglob_postfix(&mut chars) {
regex_pattern.push_str(q);
}
}
'|' => regex_pattern.push('|'),
'\\' => {
if extendedglob_on {
let mut peek = chars.clone();
let p1 = peek.next();
let p2 = peek.next();
let p3 = peek.next();
let p4 = peek.next();
if p1 == Some('(')
&& p2 == Some('#')
&& (p3 == Some('e') || p3 == Some('s'))
&& p4 == Some(')')
{
regex_pattern.push_str("\\\\");
regex_pattern.push(if p3 == Some('e') { '$' } else { '^' });
chars.next(); chars.next(); chars.next(); chars.next();
continue;
}
}
if let Some(next) = chars.next() {
if matches!(
next,
'.' | '+'
| '^'
| '$'
| '\\'
| '{'
| '}'
| '*'
| '?'
| '('
| ')'
| '|'
| '['
| ']'
) {
regex_pattern.push('\\');
}
regex_pattern.push(next);
} else {
regex_pattern.push_str("\\\\");
}
}
'.' | '+' | '^' | '$' | '{' | '}' => {
regex_pattern.push('\\');
regex_pattern.push(c);
}
_ => {
if l_flag && c.is_ascii_lowercase() {
regex_pattern.push('[');
regex_pattern.push(c);
regex_pattern.push(c.to_ascii_uppercase());
regex_pattern.push(']');
} else {
regex_pattern.push(c);
}
if let Some(q) = consume_extglob_postfix(&mut chars) {
regex_pattern.push_str(q);
}
}
}
}
regex_pattern.push('$');
let final_pattern = if case_insensitive {
format!("(?i){}", regex_pattern)
} else {
regex_pattern
};
if !numeric_ranges.is_empty() {
let re = match regex::Regex::new(&final_pattern) {
Ok(re) => re,
Err(_) => return false,
};
let caps = match re.captures(s) {
Some(c) => c,
None => return false,
};
for (group_idx, lo, hi) in numeric_ranges.iter() {
let cap_str = match caps.get(*group_idx) {
Some(m) => m.as_str(),
None => continue,
};
let n: i64 = match cap_str.parse() {
Ok(n) => n,
Err(_) => return false,
};
if let Some(l) = lo {
if n < *l {
return false;
}
}
if let Some(h) = hi {
if n > *h {
return false;
}
}
}
return true;
}
regex::Regex::new(&final_pattern)
.map(|re| re.is_match(s))
.unwrap_or(false)
}
pub fn loadautofn(shf: *mut crate::ported::zsh_h::shfunc, _ks: i32, test_only: i32, _ignore_loaddir: i32) -> i32 {
if shf.is_null() {
return 1;
}
let name = unsafe { (*shf).node.nam.clone() };
let mut dir_path: Option<String> = None;
let path = match getfpfunc(&name, &mut dir_path, None, 0) {
Some(p) => p,
None => return 1, };
if test_only != 0 { return 0; }
let body = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return 1,
};
unsafe {
(*shf).filename = dir_path.clone().or(Some(path.clone()));
}
unsafe {
(*shf).node.flags &= !(PM_UNDEFINED as i32);
}
if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
if let Some(existing) = tab.get_mut(&name) {
existing.body = Some(body);
existing.filename = dir_path;
} else {
tab.add(crate::ported::hashtable::ShFunc {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: name.clone(),
flags: 0,
},
filename: dir_path,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: Some(body),
});
}
}
0
}
pub fn getfpfunc(name: &str, dir_path_out: &mut Option<String>, spec_path: Option<&[String]>, _all_loaded: i32) -> Option<String> {
let dirs: Vec<String> = match spec_path {
Some(s) => s.to_vec(),
None => std::env::var("FPATH").or_else(|_| std::env::var("fpath"))
.ok().map(|v| v.split(':').map(String::from).collect())
.unwrap_or_default(),
};
for dir in &dirs {
if dir.is_empty() { continue; }
let path = format!("{}/{}", dir, name);
if std::path::Path::new(&path).exists() {
*dir_path_out = Some(dir.clone());
return Some(path);
}
}
None
}