use crate::history::HistoryEngine;
use crate::options::ZSH_OPTIONS_SET;
use crate::ported::builtin::{BREAKS, CONTFLAG};
use crate::ported::math::mathevali;
use crate::ported::modules::parameter::*;
use crate::ported::subst::singsub;
use crate::ported::utils::{errflag, ERRFLAG_ERROR};
use crate::ported::zsh_h::PM_UNDEFINED;
use crate::ported::zsh_h::{options, MAX_OPS};
use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED, PM_INTEGER, PM_READONLY};
use crate::ported::zsh_h::WC_SIMPLE;
use crate::compsys::cache::CompsysCache;
use crate::compsys::CompInitResult;
use parking_lot::Mutex;
use std::collections::HashSet;
use std::ffi::CStr;
use std::ffi::CString;
use std::fs;
use std::io::Read;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::io::FromRawFd;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir;
#[allow(unused_imports)]
pub(crate) use crate::func_body_fmt::FuncBodyFmt;
#[allow(unused_imports)]
pub(crate) use crate::ported::glob::expand_glob_alternation;
#[allow(unused_imports)]
pub(crate) use crate::ported::hist::bufferwords as bufferwords_z_tuple;
#[allow(unused_imports)]
pub(crate) use crate::ported::math::{parse_assign, parse_compound, parse_pre_inc};
#[allow(unused_imports)]
pub use crate::ported::params::convbase as format_int_in_base;
pub use crate::ported::params::convbase_underscore;
#[allow(unused_imports)]
pub(crate) use crate::ported::params::getarrvalue;
#[allow(unused_imports)]
pub(crate) use crate::ported::utils::base64_decode;
#[allow(unused_imports)]
pub(crate) use crate::ported::utils::{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<HashMap<String, Regex>>> =
LazyLock::new(|| Mutex::new(HashMap::with_capacity(64)));
pub(crate) use crate::fusevm_bridge::ExecutorContext;
pub use crate::fusevm_bridge::*;
pub mod zsh_version {
include!(concat!(env!("OUT_DIR"), "/zsh_version.rs"));
}
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
});
use crate::exec_jobs::{JobState, JobTable};
use crate::parse::{Redirect, RedirectOp, ShellCommand, ShellWord, VarModifier, ZshParamFlag};
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;
pub use crate::ported::builtin::AutoloadFlags;
pub use crate::ported::modules::zutil::zstyle_entry;
pub struct SubshellSnapshot {
pub paramtab: HashMap<String, crate::ported::zsh_h::Param>,
pub paramtab_hashed_storage: HashMap<String, IndexMap<String, String>>,
pub positional_params: Vec<String>,
pub env_vars: HashMap<String, String>,
pub cwd: Option<PathBuf>,
pub umask: u32,
pub traps: HashMap<String, String>,
pub opts: HashMap<String, bool>,
}
#[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 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 history: Option<HistoryEngine>,
pub(crate) process_sub_counter: u32,
pub completions: HashMap<String, CompSpec>, pub zstyles: Vec<zstyle_entry>, pub local_scope_depth: usize,
pub pending_underscore: Option<String>,
pub in_dq_context: u32,
pub in_scalar_assign: u32,
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 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 redirect_scope_stack: Vec<Vec<(i32, i32)>>,
pub redirect_failed: bool,
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_array_to_scalar: HashMap<String, (String, String)>,
}
impl ShellExecutor {
pub fn set_scalar(&mut self, name: String, value: String) {
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 {
paramtab()
.read()
.ok()
.and_then(|t| t.get(name).map(|p| p.node.flags))
.unwrap_or(0)
}
pub fn is_readonly_param(&self, name: &str) -> bool {
(self.param_flags(name) as u32 & PM_READONLY) != 0
}
pub fn last_status(&self) -> i32 {
crate::ported::builtin::LASTVAL.load(Ordering::Relaxed)
}
pub fn set_last_status(&mut self, status: i32) {
crate::ported::builtin::LASTVAL.store(status, Ordering::Relaxed);
}
pub fn set_array(&mut self, name: String, value: Vec<String>) {
setaparam(&name, value); }
pub fn set_assoc(&mut self, name: String, value: 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());
}
sethparam(&name, flat); }
pub fn scalar(&self, name: &str) -> Option<String> {
getsparam(name)
}
pub fn array(&self, name: &str) -> Option<Vec<String>> {
getaparam(name)
}
pub fn assoc(&self, name: &str) -> Option<IndexMap<String, String>> {
paramtab_hashed_storage()
.lock()
.ok()
.and_then(|m| m.get(name).cloned())
}
pub fn has_scalar(&self, name: &str) -> bool {
getsparam(name).is_some()
}
pub fn has_array(&self, name: &str) -> bool {
getaparam(name).is_some()
}
pub fn has_assoc(&self, name: &str) -> bool {
paramtab_hashed_storage()
.lock()
.ok()
.map(|m| m.contains_key(name))
.unwrap_or(false)
}
pub fn unset_assoc(&mut self, name: &str) {
unsetparam(name);
let _ = 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 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 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) {
unsetparam(name);
}
pub fn unset_scalar(&mut self, name: &str) {
unsetparam(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();
if opt_state_len() == 0 {
for (k, v) in Self::default_options() {
opt_state_set(&k, v);
}
}
setsparam("ZSH_VERSION", zsh_version::ZSH_VERSION);
setsparam("ZSH_PATCHLEVEL", zsh_version::ZSH_PATCHLEVEL);
setsparam("ZSH_NAME", "zsh");
setsparam(
"ZSH_ARGZERO",
&env::args().next().unwrap_or_else(|| "zsh".to_string()),
);
setsparam("WORDCHARS", "*?_-.[]~=/&;!#$%^(){}<>");
let shlvl = env::var("SHLVL")
.ok()
.and_then(|v| v.parse::<i32>().ok())
.map(|n| (n + 1).to_string())
.unwrap_or_else(|| "1".to_string());
setsparam("SHLVL", &shlvl);
setsparam("IFS", " \t\n\0");
setsparam("OPTIND", "1");
setsparam("OPTERR", "1");
setsparam("_", "");
let histchars_val = paramtab()
.read()
.ok()
.and_then(|t| {
t.get("histchars")
.or_else(|| t.get("HISTCHARS"))
.map(|pm| histcharsgetfn(pm))
})
.unwrap_or_else(|| "!^#".to_string());
setsparam("histchars", &histchars_val);
setsparam("MAILCHECK", "60");
setsparam("KEYTIMEOUT", "40");
setsparam("LISTMAX", "100");
setsparam("FUNCNEST", "500");
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);
let mut exec = Self {
scriptname: Some("zsh".to_string()),
scriptfilename: Some("zsh".to_string()),
subshell_snapshots: Vec::new(),
inline_env_stack: Vec::new(),
current_command_glob_failed: std::cell::Cell::new(false),
jobs: JobTable::new(),
fpath,
history,
completions: HashMap::new(),
process_sub_counter: 0,
zstyles: Vec::new(),
local_scope_depth: 0,
pending_underscore: None,
in_dq_context: 0,
in_scalar_assign: 0,
profiling_enabled: false,
compsys_cache: {
let cache_path = crate::compsys::cache::default_cache_path();
if cache_path.exists() {
let db_size = 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 _ = 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,
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,
redirect_scope_stack: Vec::new(),
redirect_failed: false,
functions_compiled: HashMap::new(),
function_source: HashMap::new(),
function_line_base: HashMap::new(),
function_def_file: HashMap::new(),
prompt_funcstack: Vec::new(),
tied_array_to_scalar: HashMap::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()));
}
for (k, v) in &arrays {
setaparam(k, v.clone()); }
{
use crate::ported::params::{paramtab, special_params};
use crate::ported::zsh_h::{PM_ARRAY, PM_DONTIMPORT, PM_SCALAR, PM_SPECIAL, PM_TIED};
if let Ok(mut tab) = paramtab().write() {
use crate::ported::zsh_h::{hashnode, param, PM_DONTIMPORT as PM_DI};
for entry in special_params.iter() {
let safe_pm_flags =
entry.pm_flags & (PM_TIED | PM_DI);
let mut bits = safe_pm_flags | PM_SPECIAL;
if entry.pm_type == PM_ARRAY {
bits |= PM_DI;
}
let _ = PM_SCALAR;
let _ = PM_DONTIMPORT;
if let Some(pm) = tab.get_mut(entry.name) {
pm.node.flags |= bits as i32;
} else {
let u_arr = if entry.pm_type == PM_ARRAY {
Some(Vec::new())
} else {
None
};
let pm: crate::ported::zsh_h::Param = Box::new(param {
node: hashnode {
next: None,
nam: entry.name.to_string(),
flags: (entry.pm_type as i32) | bits as i32,
},
u_data: 0,
u_arr,
u_str: None,
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
});
tab.insert(entry.name.to_string(), pm);
}
let _ = entry.tied_name;
}
use crate::ported::zsh_h::{PM_EXPORTED, PM_SCALAR};
use crate::ported::zsh_h::hashnode as _hn;
for (env_name, env_value) in std::env::vars() {
if env_name.is_empty() || env_name.contains('[') {
continue;
}
if env_name.as_bytes()[0].is_ascii_digit() {
continue;
}
if !crate::ported::params::isident(&env_name) {
continue;
}
if let Some(pm) = tab.get_mut(&env_name) {
pm.node.flags |= PM_EXPORTED as i32;
} else {
let pm: crate::ported::zsh_h::Param = Box::new(param {
node: _hn {
next: None,
nam: env_name.clone(),
flags: (PM_SCALAR | PM_EXPORTED) as i32,
},
u_data: 0,
u_arr: None,
u_str: Some(env_value.clone()),
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: Some(format!("{}={}", env_name, env_value)),
ename: None,
old: None,
level: 0,
});
tab.insert(env_name, pm);
}
}
}
}
init_partab_params();
let mut host_buf = [0u8; 256];
let host_rc = unsafe {
libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256)
}; if host_rc == 0 {
if let Ok(c) = std::ffi::CStr::from_bytes_until_nul(&host_buf) {
if let Ok(name) = c.to_str() {
crate::ported::params::setsparam("HOST", name); }
}
}
crate::ported::utils::set_scriptname(Some("zsh".to_string()));
crate::ported::utils::set_scriptfilename(Some("zsh".to_string()));
let mut uname_buf: libc::utsname = unsafe { std::mem::zeroed() };
let _ = unsafe { libc::uname(&mut uname_buf) };
let to_str = |b: &[libc::c_char]| -> String {
let bytes: Vec<u8> = b.iter().take_while(|&&c| c != 0).map(|&c| c as u8).collect();
String::from_utf8_lossy(&bytes).into_owned()
};
let cputype = to_str(&uname_buf.machine);
crate::ported::params::setsparam("CPUTYPE", &cputype); let sysname = to_str(&uname_buf.sysname).to_lowercase();
let release = to_str(&uname_buf.release);
let ostype = format!("{}{}", sysname, release); crate::ported::params::setsparam("OSTYPE", &ostype);
let machtype = if cputype.starts_with("arm") {
"arm".to_string()
} else {
cputype.clone()
};
crate::ported::params::setsparam("MACHTYPE", &machtype); let vendor = if sysname == "darwin" { "apple" } else { "unknown" };
crate::ported::params::setsparam("VENDOR", vendor);
let logname = unsafe {
let p = libc::getlogin();
if p.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned())
}
}; if let Some(name) = logname {
crate::ported::params::setsparam("LOGNAME", &name); }
exec
}
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"
);
return self.run_chunk(
chunk,
&format!("execute_script_file:cache:{abs_path}"),
);
}
}
}
let content =
fs::read_to_string(file_path).map_err(|e| format!("{}: {}", file_path, e))?;
let status = self.execute_script_zsh_pipeline(&content)?;
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 {
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"
);
}
}
Ok(status)
}
fn run_chunk(&mut self, chunk: fusevm::Chunk, label: &str) -> Result<i32, String> {
if chunk.ops.is_empty() {
return Ok(self.last_status());
}
crate::fusevm_disasm::maybe_print_stdout(label, &chunk);
let mut vm = fusevm::VM::new(chunk);
register_builtins(&mut vm);
vm.last_status = self.last_status();
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 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);
let status = self.run_chunk(chunk, "execute_script_zsh_pipeline")?;
let exit_body = crate::ported::builtin::traps_table()
.lock()
.ok()
.and_then(|mut t| t.remove("EXIT"));
if let Some(action) = exit_body {
tracing::debug!("firing EXIT trap (new pipeline)");
let _ = self.execute_script_zsh_pipeline(&action);
self.set_last_status(status);
}
let _ = status;
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_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> {
if !self.functions_compiled.contains_key(name) {
if let Some(stub) = crate::ported::utils::getshfunc(name) {
if (stub.node.flags as u32 & PM_UNDEFINED) != 0 {
let boxed = Box::new(stub.clone());
let ptr = Box::into_raw(boxed);
let _ = crate::ported::exec::loadautofn(ptr, 0, 0, 0);
unsafe {
let _ = Box::from_raw(ptr);
}
if let Some(body) =
crate::ported::utils::getshfunc(name).and_then(|f| f.body)
{
let wrapped = format!("{name}() {{\n{body}\n}}");
let _ = self.execute_script_zsh_pipeline(&wrapped);
}
} else if let Some(body) = stub.body.clone() {
let wrapped = format!("{name}() {{\n{body}\n}}");
let _ = self.execute_script_zsh_pipeline(&wrapped);
}
}
}
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 display_name = if name.starts_with("_zshrs_anon_") {
"(anon)".to_string()
} else {
name.to_string()
};
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));
self.local_scope_depth += 1;
let mut synth_shf = crate::ported::zsh_h::shfunc {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: display_name.clone(),
flags: 0,
},
filename: self.function_def_file.get(name).cloned().flatten(),
lineno: self.function_line_base.get(name).copied().unwrap_or(0) as i64,
funcdef: None,
redir: None,
sticky: None,
body: None,
};
let mut doshargs: Vec<String> = vec![display_name.clone()];
doshargs.extend(args.iter().cloned());
crate::fusevm_disasm::maybe_print_stdout(&format!("function:{name}"), &chunk);
let chunk_for_run = chunk.clone();
let seed_status = self.last_status();
let body_runner = move || -> i32 {
let mut vm = fusevm::VM::new(chunk_for_run.clone());
register_builtins(&mut vm);
vm.last_status = seed_status;
let _ = vm.run();
vm.last_status
};
let _ctx = ExecutorContext::enter(self);
let status = crate::ported::exec::doshfunc(
&mut synth_shf,
doshargs,
false,
body_runner,
);
drop(_ctx);
self.prompt_funcstack.pop();
self.local_scope_depth -= 1;
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))
}
}
}
}
}
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);
singsub(&untoked)
})
.collect()
} else {
Vec::new()
}
}
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('~') {
singsub(filename)
} else {
filename.to_string()
};
let resolved = resolved.to_string();
match 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);
}
cmdpush(crate::ported::zsh_h::CS_CMDSUBST as u8); let saved_lineno = 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() {
crate::fusevm_disasm::maybe_print_stdout("run_command_substitution", &chunk);
let paramtab_snap = crate::ported::params::paramtab()
.read()
.ok()
.map(|t| t.clone())
.unwrap_or_default();
let paramtab_hashed_snap =
crate::ported::params::paramtab_hashed_storage()
.lock()
.ok()
.map(|m| m.clone())
.unwrap_or_default();
let pparams_snap = self.pparams();
let opts_snap = crate::ported::options::opt_state_snapshot();
let traps_snap = crate::ported::builtin::traps_table()
.lock()
.map(|t| t.clone())
.unwrap_or_default();
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 Ok(mut t) = crate::ported::params::paramtab().write() {
*t = paramtab_snap;
}
if let Ok(mut m) = crate::ported::params::paramtab_hashed_storage().lock() {
*m = paramtab_hashed_snap;
}
self.set_pparams(pparams_snap);
crate::ported::options::opt_state_restore(opts_snap);
if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
*t = traps_snap;
}
}
}
if let Some(ln) = saved_lineno {
self.set_scalar("LINENO".to_string(), ln);
}
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 { File::from_raw_fd(read_fd) };
let mut output = String::new();
let _ = io::BufReader::new(read_file).read_to_string(&mut output);
while output.ends_with('\n') {
output.pop();
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_echo() {
let _g = crate::test_util::global_state_lock();
let mut exec = ShellExecutor::new();
let status = exec.execute_script("true").unwrap();
assert_eq!(status, 0);
}
#[test]
fn test_if_true() {
let _g = crate::test_util::global_state_lock();
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 _g = crate::test_util::global_state_lock();
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 _g = crate::test_util::global_state_lock();
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 _g = crate::test_util::global_state_lock();
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 _g = crate::test_util::global_state_lock();
let mut exec = ShellExecutor::new();
let status = exec.execute_script("false || true").unwrap();
assert_eq!(status, 0);
}
#[test]
fn test_forklevel_default_zero_and_roundtrip() {
let _g = crate::test_util::global_state_lock();
use std::sync::atomic::Ordering;
let prev = crate::ported::exec::FORKLEVEL.load(Ordering::Relaxed);
crate::ported::exec::FORKLEVEL.store(0, Ordering::Relaxed);
assert_eq!(crate::ported::exec::FORKLEVEL.load(Ordering::Relaxed), 0);
crate::ported::exec::FORKLEVEL.store(3, Ordering::Relaxed);
assert_eq!(crate::ported::exec::FORKLEVEL.load(Ordering::Relaxed), 3);
crate::ported::exec::FORKLEVEL.store(prev, Ordering::Relaxed);
}
}
#[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(),
);
}
}
}
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,
}
use crate::fusevm_bridge::with_executor;
use crate::ported::glob::*;
use crate::ported::hist::*;
use crate::ported::jobs::*;
use crate::ported::math::*;
use crate::ported::module::*;
use crate::ported::modules::cap::*;
use crate::ported::modules::terminfo::*;
use crate::ported::options::*;
use crate::ported::params::*;
use crate::ported::pattern::*;
use crate::ported::prompt::*;
use crate::ported::signals::*;
use crate::ported::subst::*;
use crate::ported::utils::{zerr, zerrnam, zwarn, zwarnnam};
use ::regex::{Error as RegexError, Regex, RegexBuilder};
impl ShellExecutor {
pub(crate) fn all_zsh_options() -> Vec<&'static str> {
ZSH_OPTIONS_SET.iter().copied().collect()
}
pub(crate) fn default_options() -> HashMap<String, bool> {
let on = default_on_options();
Self::all_zsh_options()
.into_iter()
.map(|n| (n.to_string(), on.contains(n)))
.collect()
}
}
impl ShellExecutor {
pub(crate) fn get_variable(&self, name: &str) -> String {
getsparam(name).unwrap_or_default()
}
}
impl ShellExecutor {
pub fn run_trap(&mut self, signal: &str) {
let action = crate::ported::builtin::traps_table()
.lock()
.ok()
.and_then(|t| t.get(signal).cloned());
if let Some(body) = action {
if !body.is_empty() {
let _ = self.execute_script(&body);
}
}
}
}
impl ShellExecutor {
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 ShellExecutor {
pub fn expand_glob(&self, pattern: &str) -> Vec<String> {
let expanded = glob_path(pattern);
if !expanded.is_empty() {
return expanded;
}
let per_glob_nullglob = crate::ported::glob::parse_qualifiers(pattern)
.1
.map(|q| q.nullglob)
.unwrap_or(false);
let nullglob = opt_state_get("nullglob").unwrap_or(false) || per_glob_nullglob;
if nullglob {
return Vec::new();
}
let nomatch = opt_state_get("nomatch").unwrap_or(true);
if nomatch && Self::looks_like_glob(pattern) {
zerr(&format!("no matches found: {}", pattern)); errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); self.current_command_glob_failed.set(true);
return Vec::new(); }
vec![pattern.to_string()]
}
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
}
}
impl ShellExecutor {
pub(crate) fn copy_dir_recursive(
src: &Path,
dest: &Path,
) -> io::Result<()> {
if !dest.exists() {
fs::create_dir_all(dest)?;
}
for entry in 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 {
fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
}
use std::cell::RefCell;
thread_local! {
static SCAN_KEYS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
pub fn partab_get(name: &str, key: &str) -> Option<String> {
for entry in PARTAB.iter() {
if entry.name == name {
return (entry.getfn)(std::ptr::null_mut(), key).and_then(|p| p.u_str);
}
}
None
}
pub fn partab_array_get(name: &str) -> Option<Vec<String>> {
for entry in PARTAB_ARRAY.iter() {
if entry.name == name {
return Some((entry.getfn)(std::ptr::null_mut()));
}
}
None
}
pub fn partab_scan_keys(name: &str) -> Option<Vec<String>> {
for entry in PARTAB.iter() {
if entry.name == name {
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()));
}
(entry.scanfn)(std::ptr::null_mut(), Some(cb), 0);
return Some(SCAN_KEYS.with(|k| k.borrow().clone()));
}
}
None
}pub fn init_partab_params() {
use crate::ported::modules::parameter::{PARTAB, PARTAB_ARRAY};
use crate::ported::zsh_h::{hashnode, param, Param, PM_HIDE, PM_HIDEVAL, PM_READONLY, PM_SPECIAL};
let mut tab = match paramtab().write() {
Ok(t) => t,
Err(_) => return,
};
let mk_pm = |name: &str, flags: i32| -> Param {
Box::new(param {
node: hashnode {
next: None,
nam: name.to_string(),
flags: (flags & !(PM_READONLY as i32))
| PM_SPECIAL as i32
| PM_HIDE as i32
| PM_HIDEVAL as i32,
},
u_data: 0,
u_arr: None,
u_str: None,
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
})
};
for entry in PARTAB.iter() {
tab.insert(entry.name.to_string(), mk_pm(entry.name, entry.flags));
}
for entry in PARTAB_ARRAY.iter() {
tab.insert(entry.name.to_string(), mk_pm(entry.name, entry.flags));
}
}
impl 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));
crate::ported::options::emulate("sh", true);
}
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));
crate::ported::options::emulate("ksh", true);
}
}
pub fn glob_match_static(s: &str, pattern: &str) -> bool {
let matched = patcompile(pattern, PAT_HEAPDUP as i32, None)
.map_or(false, |p| pattry(&p, s));
if matched && pattern.contains("(#m)") {
crate::ported::params::setsparam("MATCH", s);
crate::ported::params::setiparam("MBEGIN", 1);
crate::ported::params::setiparam(
"MEND",
s.chars().count() as i64,
);
}
matched
}