use std::path::Path;
pub use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED, PM_INTEGER, PM_READONLY, PM_UNIQUE};
pub fn declare_locals(names: &[&str], kind: u32) {
use crate::ported::params::{createparam, locallevel, paramtab};
use crate::ported::zsh_h::{PM_HIDE, PM_LOCAL, PM_SPECIAL};
use std::sync::atomic::Ordering;
let cur = locallevel.load(Ordering::Relaxed); if cur == 0 {
return;
}
for name in names {
let (needs_shadow, newspecial) = paramtab()
.read()
.ok()
.and_then(|t| {
t.get(*name).map(|pm| {
let special = (pm.node.flags as u32 & PM_SPECIAL) != 0
&& (kind & PM_HIDE) == 0
&& (pm.node.flags as u32 & PM_HIDE) == 0;
(pm.level < cur, special)
})
})
.unwrap_or((true, false));
if !needs_shadow {
continue;
}
let _ = createparam(name, (kind | PM_LOCAL) as i32);
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(*name) {
pm.level = cur;
pm.node.flags |= (kind & PM_UNIQUE) as i32;
if newspecial {
pm.node.flags |= PM_SPECIAL as i32;
}
}
}
}
}
pub struct LocalScope {
saved: Vec<(String, Option<Box<crate::ported::zsh_h::param>>)>,
saved_hash: Vec<(String, Option<indexmap::IndexMap<String, String>>)>,
}
impl LocalScope {
pub fn declare(names: &[&str], kind: u32) -> Self {
let mut scope = LocalScope {
saved: Vec::new(),
saved_hash: Vec::new(),
};
scope.also(names, kind);
scope
}
fn remember_hash(&mut self, names: &[&str]) {
if let Ok(store) = crate::ported::params::paramtab_hashed_storage().lock() {
for name in names {
self.saved_hash
.push(((*name).to_string(), store.get(*name).cloned()));
}
}
}
pub fn also(&mut self, names: &[&str], kind: u32) {
if let Ok(tab) = crate::ported::params::paramtab().read() {
for name in names {
self.saved
.push(((*name).to_string(), tab.get(*name).cloned()));
}
}
self.remember_hash(names);
declare_locals(names, kind);
}
pub fn also_keeping_value(&mut self, names: &[&str]) {
if let Ok(tab) = crate::ported::params::paramtab().read() {
for name in names {
self.saved
.push(((*name).to_string(), tab.get(*name).cloned()));
}
}
self.remember_hash(names);
declare_locals_keeping_value(names);
}
}
impl Drop for LocalScope {
fn drop(&mut self) {
if let Ok(mut tab) = crate::ported::params::paramtab().write() {
for (name, prev) in self.saved.iter().rev() {
match prev {
Some(pm) => {
tab.insert(name.clone(), pm.clone());
}
None => {
tab.remove(name);
}
}
}
}
if let Ok(mut store) = crate::ported::params::paramtab_hashed_storage().lock() {
for (name, prev) in self.saved_hash.iter().rev() {
match prev {
Some(map) => {
store.insert(name.clone(), map.clone());
}
None => {
store.remove(name);
}
}
}
}
}
}
pub fn mark_readonly(names: &[&str]) {
use crate::ported::params::{locallevel, paramtab};
use crate::ported::zsh_h::PM_READONLY;
use std::sync::atomic::Ordering;
if locallevel.load(Ordering::Relaxed) == 0 {
return;
}
if let Ok(mut tab) = paramtab().write() {
for name in names {
if let Some(pm) = tab.get_mut(*name) {
pm.node.flags |= PM_READONLY as i32;
}
}
}
}
fn empty_ops() -> crate::ported::zsh_h::options {
crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
pub fn zstyle_t(ctx: &str, style: &str) -> i32 {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-t".to_string(), ctx.to_string(), style.to_string()],
&empty_ops(),
0,
)
}
pub fn zstyle_s(ctx: &str, style: &str) -> Option<String> {
let vals = crate::ported::modules::zutil::lookupstyle(ctx, style);
if vals.is_empty() {
return None; }
Some(crate::ported::utils::zjoin(&vals, ' '))
}
#[allow(non_snake_case)]
pub fn zstyle_T(ctx: &str, style: &str) -> i32 {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-T".to_string(), ctx.to_string(), style.to_string()],
&empty_ops(),
0,
)
}
pub fn set_bridge_argv(name: &str, args: &[String]) {
declare_locals(&[name], PM_ARRAY);
let _ = crate::ported::params::setaparam(name, args.to_vec());
}
pub fn declare_locals_keeping_value(names: &[&str]) {
for name in names {
let inherited = crate::ported::params::getsparam(name);
declare_locals(&[name], 0);
if let Some(v) = inherited {
let _ = crate::ported::params::setsparam(name, &v);
}
}
}
pub fn compinit_scan_dirs(env_fpath: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
match crate::ported::params::getaparam("fpath") {
Some(live) if !live.is_empty() => live.iter().map(std::path::PathBuf::from).collect(),
_ => env_fpath.to_vec(),
}
}
pub fn is_executable(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = path.metadata() {
let mode = meta.permissions().mode();
return mode & 0o111 != 0;
}
}
#[cfg(not(unix))]
{
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
return matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com");
}
}
false
}
pub fn glob_matches(pattern: &str, text: &str) -> bool {
if let Some(rest) = pattern.strip_prefix('(') {
if let Some(close) = find_top_close_paren(rest) {
let group = &rest[..close];
let after = &rest[close + 1..];
return group.split('|').any(|alt| {
let combined = format!("{}{}", alt, after);
glob_matches(&combined, text)
});
}
}
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
glob_helper(&pat, &txt)
}
fn find_top_close_paren(s: &str) -> Option<usize> {
let mut depth: i32 = 1;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn glob_helper(pat: &[char], txt: &[char]) -> bool {
if pat.is_empty() {
return txt.is_empty();
}
if pat[0] == '(' {
let rest: String = pat[1..].iter().collect();
let txt_str: String = txt.iter().collect();
if let Some(close) = find_top_close_paren(&rest) {
let group = &rest[..close];
let after = &rest[close + 1..];
return group.split('|').any(|alt| {
let combined = format!("{}{}", alt, after);
glob_matches(&combined, &txt_str)
});
}
}
match pat[0] {
'*' => {
for i in 0..=txt.len() {
if glob_helper(&pat[1..], &txt[i..]) {
return true;
}
}
false
}
'?' => !txt.is_empty() && glob_helper(&pat[1..], &txt[1..]),
c => !txt.is_empty() && txt[0] == c && glob_helper(&pat[1..], &txt[1..]),
}
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
glob_matches(pattern, text)
}
pub fn edit_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
let mut dp = vec![vec![0; n + 1]; m + 1];
#[allow(clippy::needless_range_loop)]
for i in 0..=m {
dp[i][0] = i;
}
#[allow(clippy::needless_range_loop)]
for j in 0..=n {
dp[0][j] = j;
}
for i in 1..=m {
for j in 1..=n {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + cost);
}
}
dp[m][n]
}
pub fn is_ignored(s: &str, patterns: &[String]) -> bool {
for pattern in patterns {
if glob_match(pattern, s) {
return true;
}
}
false
}
pub fn get_ignored_patterns(context: &str) -> Vec<String> {
crate::ported::modules::zutil::lookupstyle(context, "ignored-patterns")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dequote_q_recovers_the_command_name_from_a_quoted_word() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dequote_q("\"env\""), "env");
assert_eq!(dequote_q("'rm'"), "rm");
assert_eq!(dequote_q("\\rm"), "rm");
assert_eq!(dequote_q("/usr/bin/ls"), "/usr/bin/ls");
assert_eq!(dequote_q("-default-"), "-default-");
}
#[test]
fn dequote_q_keeps_an_unbalanced_quote_as_a_literal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dequote_q("x\"y"), "x\"y");
assert_eq!(dequote_q("x'y"), "x'y");
}
#[test]
fn bridge_argv_is_declared_local_not_created_global() {
let _g = crate::test_util::global_state_lock();
crate::ported::utils::inc_locallevel();
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
set_bridge_argv("__compsys_argv", &["-J".to_string(), "grp".to_string()]);
let ty = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get("__compsys_argv")
.map(|pm| crate::ported::modules::parameter::paramtypestr(pm))
})
.unwrap_or_default();
assert_eq!(
ty, "array-local",
"bridge argv must be `local -a`; `array` means every call \
re-creates a global and WARN_CREATE_GLOBAL prints a line"
);
assert_eq!(
crate::ported::params::getaparam("__compsys_argv").unwrap_or_default(),
vec!["-J".to_string(), "grp".to_string()],
"declaring it local must not cost the value zparseopts reads"
);
}));
crate::ported::params::endparamscope();
let _ = crate::ported::params::unsetparam("__compsys_argv");
if let Err(p) = out {
std::panic::resume_unwind(p);
}
}
#[test]
fn declare_locals_carries_the_shell_kind_and_unwinds() {
let _g = crate::test_util::global_state_lock();
crate::ported::utils::inc_locallevel();
let cases: [(&str, u32, &str); 4] = [
("zzlk_scalar", 0, "scalar"),
("zzlk_array", PM_ARRAY, "array"),
("zzlk_assoc", PM_HASHED, "association"),
("zzlk_uniq", PM_ARRAY | PM_UNIQUE, "unique"),
];
let inner = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
for (name, kind, want) in cases {
declare_locals(&[name], kind);
let ty = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get(name)
.map(|pm| crate::ported::modules::parameter::paramtypestr(pm))
})
.unwrap_or_default();
assert!(
ty.contains(want) && ty.contains("local"),
"declare_locals({name}, {kind:#x}) produced `{ty}`, \
expected it to contain `{want}` and `local`"
);
}
}));
crate::ported::params::endparamscope();
let survivors: Vec<&str> = cases
.iter()
.map(|(n, _, _)| *n)
.filter(|n| {
crate::ported::params::paramtab()
.read()
.map(|t| t.get(*n).is_some())
.unwrap_or(false)
})
.collect();
for (n, _, _) in cases {
let _ = crate::ported::params::unsetparam(n);
}
if let Err(p) = inner {
std::panic::resume_unwind(p);
}
assert!(
survivors.is_empty(),
"{survivors:?} outlived endparamscope, i.e. pm->level was never \
stamped and every port that declares a name this way still leaks it"
);
}
#[test]
fn compinit_scans_the_live_fpath_array_not_the_startup_env() {
use std::path::PathBuf;
let _g = crate::test_util::global_state_lock();
let env_seeded = vec![PathBuf::from("/from/FPATH/env")];
crate::ported::params::setaparam(
"fpath",
vec!["/live/one".to_string(), "/live/two".to_string()],
);
assert_eq!(
compinit_scan_dirs(&env_seeded),
vec![PathBuf::from("/live/one"), PathBuf::from("/live/two")],
"sh:523 scans $fpath"
);
crate::ported::params::setaparam("fpath", Vec::new());
assert_eq!(compinit_scan_dirs(&env_seeded), env_seeded);
crate::ported::params::unsetparam("fpath");
assert_eq!(compinit_scan_dirs(&env_seeded), env_seeded);
}
#[test]
fn mark_readonly_is_scoped_to_a_function() {
use crate::ported::modules::parameter::paramtypestr;
let _g = crate::test_util::global_state_lock();
crate::ported::params::locallevel.store(0, std::sync::atomic::Ordering::Relaxed);
let type_of = |n: &str| {
crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get(n).map(|pm| paramtypestr(pm)))
.unwrap_or_default()
};
crate::ported::params::setaparam("_ro_probe", vec!["a".to_string()]);
mark_readonly(&["_ro_probe"]);
assert_eq!(type_of("_ro_probe"), "array", "no scope — no readonly bit");
crate::ported::utils::inc_locallevel();
declare_locals(&["_ro_probe"], PM_ARRAY);
crate::ported::params::setaparam("_ro_probe", vec!["b".to_string()]);
mark_readonly(&["_ro_probe"]);
assert_eq!(type_of("_ro_probe"), "array-local-readonly");
crate::ported::params::endparamscope();
assert_eq!(
type_of("_ro_probe"),
"array",
"endparamscope must unwind the readonly shadow"
);
crate::ported::params::unsetparam("_ro_probe");
}
#[test]
fn test_glob_match_simple() {
assert!(glob_match("*.txt", "file.txt"));
assert!(glob_match("*.txt", ".txt"));
assert!(!glob_match("*.txt", "file.rs"));
}
#[test]
fn test_glob_match_question() {
assert!(glob_match("file?.txt", "file1.txt"));
assert!(glob_match("file?.txt", "fileX.txt"));
assert!(!glob_match("file?.txt", "file.txt"));
assert!(!glob_match("file?.txt", "file12.txt"));
}
#[test]
fn test_glob_match_star_middle() {
assert!(glob_match("foo*bar", "foobar"));
assert!(glob_match("foo*bar", "foo123bar"));
assert!(glob_match("foo*bar", "fooXYZbar"));
assert!(!glob_match("foo*bar", "foobaz"));
}
#[test]
fn test_glob_match_multiple_stars() {
assert!(glob_match("*foo*", "foo"));
assert!(glob_match("*foo*", "afoo"));
assert!(glob_match("*foo*", "foob"));
assert!(glob_match("*foo*", "afoob"));
assert!(!glob_match("*foo*", "bar"));
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("exact", "exact"));
assert!(!glob_match("exact", "exacty"));
assert!(!glob_match("exact", "xact"));
}
}
pub fn call_compfn(name: &str, args: &[String], fallback: impl FnOnce() -> i32) -> i32 {
crate::ported::exec::dispatch_function_call(name, args).unwrap_or_else(fallback)
}
pub struct FnScope {
saved: Option<String>,
saved_lineno: u64,
}
impl FnScope {
pub fn enter(name: &str) -> Self {
let saved = crate::ported::utils::scriptname_get();
crate::ported::utils::set_scriptname(Some(name.to_string()));
let saved_lineno = crate::ported::lex::lineno();
crate::ported::lex::set_lineno(0);
FnScope {
saved,
saved_lineno,
}
}
}
pub struct PortFuncstackFrame;
impl PortFuncstackFrame {
pub fn push(name: &str) -> Self {
if let Ok(mut stack) = crate::ported::modules::parameter::FUNCSTACK.lock() {
stack.push(crate::ported::zsh_h::funcstack {
prev: None, name: name.to_string(), filename: None, caller: None, flineno: 0, lineno: crate::ported::lex::lineno() as i64, tp: crate::ported::zsh_h::FS_FUNC, });
}
PortFuncstackFrame
}
}
impl Drop for PortFuncstackFrame {
fn drop(&mut self) {
if let Ok(mut stack) = crate::ported::modules::parameter::FUNCSTACK.lock() {
stack.pop();
}
}
}
impl Drop for FnScope {
fn drop(&mut self) {
crate::ported::utils::set_scriptname(self.saved.take());
crate::ported::lex::set_lineno(self.saved_lineno);
}
}
pub fn set_sh_lineno(line: u64) {
crate::ported::lex::set_lineno(line);
}
pub fn getopts_loop(
args: &[String],
optstring: &str,
sh_line: u64,
mut on_opt: impl FnMut(&str, Option<&str>),
) -> usize {
use crate::ported::params::setsparam;
let quiet = optstring.starts_with(':'); let optstr = &optstring[quiet as usize..]; let posix = crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS); let warn = |msg: String| {
set_sh_lineno(sh_line);
crate::ported::utils::zwarn(&msg);
};
let mut zoptind = 1usize; let mut optcind = 0usize; loop {
if args.len() < zoptind {
return zoptind - 1; }
let mut str: Vec<char> = args[zoptind - 1].chars().collect();
if str.is_empty() {
return zoptind - 1; }
if optcind >= str.len() {
optcind = 0;
zoptind += 1;
if args.len() < zoptind {
return zoptind - 1;
}
str = args[zoptind - 1].chars().collect();
}
if optcind == 0 {
if str.len() < 2 || (str[0] != '-' && (posix || str[0] != '+')) {
return zoptind - 1;
}
if str.len() == 2 && str[0] == '-' && str[1] == '-' {
zoptind += 1;
return zoptind - 1;
}
optcind = 1;
}
let opch = str[optcind]; optcind += 1; let sign = if str[0] == '+' { "+" } else { "-" }; let optbuf = if str[0] == '+' { format!("+{}", opch) } else { opch.to_string() };
let pos = if opch == ':' { None } else { optstr.find(opch) };
let Some(pos) = pos else {
if posix {
optcind = 0;
zoptind += 1;
}
if quiet {
let _ = setsparam("OPTARG", &format!("{}{}", sign, opch)); } else {
warn(format!("bad option: {}{}", sign, opch)); let _ = setsparam("OPTARG", ""); }
on_opt("?", None); continue;
};
if optstr.as_bytes().get(pos + opch.len_utf8()) == Some(&b':') {
let optarg: String = if optcind == str.len() {
if args.len() <= zoptind {
if posix {
optcind = 0;
zoptind += 1;
}
if quiet {
let _ = setsparam("OPTARG", &format!("{}{}", sign, opch));
on_opt(":", None);
} else {
let _ = setsparam("OPTARG", "");
warn(format!("argument expected after {}{} option", sign, opch));
on_opt("?", None);
}
continue;
}
zoptind += 1; args[zoptind - 1].clone()
} else {
str[optcind..].iter().collect() };
optcind = 0; zoptind += 1; let _ = setsparam("OPTARG", &optarg); on_opt(&optbuf, Some(&optarg)); } else {
let _ = setsparam("OPTARG", ""); on_opt(&optbuf, None); }
}
}
pub fn cmdsubst_external_words(cmd: &str, args: &[&str], sh_line: u64) -> Vec<String> {
let out = std::process::Command::new(cmd)
.args(args)
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::inherit())
.output();
match out {
Ok(o) => String::from_utf8_lossy(&o.stdout)
.split_whitespace()
.map(String::from)
.collect(),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
set_sh_lineno(sh_line);
let _subsh = crate::ported::exec::SubshStateGuard::enter(); crate::ported::utils::zwarn(&format!("command not found: {}", cmd)); }
Vec::new()
}
}
}
pub fn eval_comp(comp: &str, line: u64) -> i32 {
set_sh_lineno(line);
let oscriptname = crate::ported::utils::scriptname_get(); let fstack = crate::ported::exec::EvalFuncstackFrame::push(); if fstack.pushed() {
crate::ported::utils::set_scriptname(Some("(eval)".to_string()));
}
let mut lastval = crate::ported::exec::execute_script(comp).unwrap_or(1);
{
let ef = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
if ef != 0 && lastval == 0 {
lastval = ef;
}
}
drop(fstack); crate::ported::utils::errflag.fetch_and(
!crate::ported::zsh_h::ERRFLAG_ERROR,
std::sync::atomic::Ordering::Relaxed,
);
crate::ported::utils::set_scriptname(oscriptname); lastval }
pub fn dispatch_action_command(cmd: &str, argv: &[String], line: u64) -> i32 {
set_sh_lineno(line);
if cmd == "compadd" {
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
return crate::ported::zle::complete::bin_compadd("compadd", argv, &ops, 0);
}
if let Some(rc) = crate::ported::exec::dispatch_function_call(cmd, argv) {
return rc;
}
if let Some(slash) = cmd.find('/') {
use crate::ported::zsh_h::{isset, PATHDIRS};
let dot = cmd.starts_with('.') && (slash == 1 || (cmd.starts_with("..") && slash == 2));
if slash == 0 || !isset(PATHDIRS) || dot {
let lerrno = direct_exec_errno(cmd);
if lerrno == 0 {
return 1;
}
let _subsh = crate::ported::exec::SubshStateGuard::enter(); crate::ported::utils::zwarn(&format!(
"{}: {}",
crate::ported::utils::zsh_errno_msg(lerrno),
cmd
)); return if lerrno == libc::EACCES || lerrno == libc::ENOEXEC { 126 } else { 127 }; }
}
if crate::ported::builtin::createbuiltintable().contains_key(cmd)
|| crate::ported::exec::findcmd(cmd, 0, 0).is_some()
{
return 1;
}
let mut eno = 0;
for pp in crate::ported::params::getsparam("PATH").unwrap_or_default().split(':') {
let candidate = if pp.is_empty() || pp == "." {
cmd.to_string() } else {
format!("{}/{}", pp, cmd) };
let ee = direct_exec_errno(&candidate);
if ee == 0 {
return 1;
}
if crate::ported::exec::isgooderr(ee, pp) {
eno = ee; }
}
if eno != 0 {
let _subsh = crate::ported::exec::SubshStateGuard::enter(); crate::ported::utils::zwarn(&format!(
"{}: {}",
crate::ported::utils::zsh_errno_msg(eno),
cmd
)); return if eno == libc::EACCES || eno == libc::ENOEXEC { 126 } else { 127 }; }
let _subsh = crate::ported::exec::SubshStateGuard::enter(); crate::ported::utils::zwarn(&format!("command not found: {}", cmd)); 127 }
fn direct_exec_errno(path: &str) -> i32 {
let Ok(c) = std::ffi::CString::new(path) else {
return libc::ENOENT;
};
if unsafe { libc::access(c.as_ptr(), libc::X_OK) } != 0 {
return std::io::Error::last_os_error().raw_os_error().unwrap_or(libc::ENOENT);
}
match std::fs::metadata(path) {
Ok(m) if m.is_file() => 0,
Ok(_) => libc::EACCES,
Err(e) => e.raw_os_error().unwrap_or(libc::ENOENT),
}
}
pub fn capture_builtin_stdout(discard_stderr: bool, run: impl FnOnce()) -> String {
let (fd, path) = match crate::ported::utils::gettempfile(None) {
Some(t) => t,
None => return String::new(),
};
let _ = std::io::Write::flush(&mut std::io::stdout());
let saved_out = unsafe { libc::dup(1) };
let saved_err = if discard_stderr {
unsafe { libc::dup(2) }
} else {
-1
};
let devnull = if discard_stderr {
unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY) }
} else {
-1
};
let redirected = saved_out >= 0 && unsafe { libc::dup2(fd, 1) } >= 0;
if redirected {
let err_redirected =
devnull >= 0 && saved_err >= 0 && unsafe { libc::dup2(devnull, 2) } >= 0;
run();
let _ = std::io::Write::flush(&mut std::io::stdout());
unsafe {
libc::dup2(saved_out, 1);
}
if err_redirected {
unsafe {
libc::dup2(saved_err, 2);
}
}
}
for f in [saved_out, saved_err, devnull, fd] {
if f >= 0 {
unsafe {
libc::close(f);
}
}
}
let text = if redirected {
std::fs::read_to_string(&path).unwrap_or_default()
} else {
String::new()
};
let _ = std::fs::remove_file(&path);
text
}
#[cfg(test)]
mod capture_builtin_stdout_tests {
use super::*;
fn write_fd(fd: i32, s: &str) {
unsafe {
libc::write(fd, s.as_ptr() as *const libc::c_void, s.len());
}
}
#[test]
fn captures_stdout_and_restores_fd1() {
let _g = crate::test_util::global_state_lock();
let text = capture_builtin_stdout(false, || {
write_fd(1, "one\ntwo\n");
});
assert_eq!(text, "one\ntwo\n");
assert!(
unsafe { libc::fcntl(1, libc::F_GETFD) } >= 0,
"fd 1 was not restored"
);
}
#[test]
fn discard_stderr_leaves_fd2_usable() {
let _g = crate::test_util::global_state_lock();
let text = capture_builtin_stdout(true, || {
write_fd(1, "out\n");
write_fd(2, "this must not be captured\n");
});
assert_eq!(text, "out\n");
assert!(
unsafe { libc::fcntl(2, libc::F_GETFD) } >= 0,
"fd 2 was not restored"
);
}
}
#[cfg(test)]
mod lineno_scope_tests {
use super::*;
use crate::ported::lex::{lineno, set_lineno};
#[test]
fn getopts_loop_clusters_and_recovers_like_bin_getopts() {
let _g = crate::test_util::global_state_lock();
let run = |args: &[&str], optstring: &str| {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let mut seen: Vec<String> = Vec::new();
let n = getopts_loop(&args, optstring, 7, |opt, optarg| {
seen.push(format!("{}={}", opt, optarg.unwrap_or("")));
});
(n, seen)
};
let (n, seen) = run(&["-J", "-default-", "t:d:(a b)"], "O:C:");
assert_eq!(n, 2);
assert_eq!(seen.len(), 9, "{:?}", seen);
assert!(seen.iter().all(|s| s == "?="));
let (n, seen) = run(&["-Oargs", "-C", "ctx", "--", "-"], "O:C:");
assert_eq!(n, 4);
assert_eq!(seen, ["O=args", "C=ctx"]);
let (n, seen) = run(&["+x", "-t"], "t:x");
assert_eq!(n, 2);
assert_eq!(seen, ["+x=", "?="]);
let (n, seen) = run(&["-ot-", "rest"], "oOt:12JVx");
assert_eq!(n, 1);
assert_eq!(seen, ["o=", "t=-"]);
}
#[test]
fn fn_scope_zeroes_lineno_for_the_port_body() {
let _g = crate::test_util::global_state_lock();
set_lineno(218); {
let _s = FnScope::enter("_describe");
assert_eq!(lineno(), 0, "port body must start with no known line");
}
}
#[test]
fn fn_scope_restores_the_callers_lineno_on_exit() {
let _g = crate::test_util::global_state_lock();
set_lineno(218);
{
let _s = FnScope::enter("_describe");
set_sh_lineno(129);
assert_eq!(lineno(), 129);
}
assert_eq!(lineno(), 218, "caller's line must survive the port call");
}
#[test]
fn nested_fn_scopes_unwind_to_the_right_line() {
let _g = crate::test_util::global_state_lock();
set_lineno(0);
let outer = FnScope::enter("_describe");
set_sh_lineno(122);
{
let _inner = FnScope::enter("_tags");
assert_eq!(lineno(), 0);
set_sh_lineno(36); assert_eq!(lineno(), 36);
}
assert_eq!(lineno(), 122, "_describe's line must survive _tags");
drop(outer);
assert_eq!(lineno(), 0);
}
}
#[cfg(test)]
mod diagnostic_framing_tests {
use std::sync::atomic::Ordering;
#[test]
fn the_not_found_diagnostic_does_not_trash_the_line_editor() {
use crate::ported::builtins::sched::zleactive;
use crate::ported::init::zle_load_state;
use crate::ported::zle::zle_refresh::{RESETNEEDED, TRASHEDZLE};
let _g = crate::test_util::global_state_lock();
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut::<libc::termios>(),
std::ptr::null_mut::<libc::winsize>(),
)
};
assert_eq!(rc, 0, "openpty failed; the probe needs a terminal on fd 2");
let saved_stderr = unsafe { libc::dup(2) };
assert!(saved_stderr >= 0, "dup(2) failed");
assert!(
unsafe { libc::dup2(slave, 2) } >= 0,
"dup2 onto fd 2 failed"
);
assert_eq!(
unsafe { libc::isatty(2) },
1,
"fd 2 must be a terminal or Src/utils.c:144 skips the hook"
);
let saved = (
zleactive.load(Ordering::Relaxed),
zle_load_state.load(Ordering::SeqCst),
TRASHEDZLE.load(Ordering::Relaxed),
RESETNEEDED.load(Ordering::Relaxed),
);
zleactive.store(1, Ordering::Relaxed);
zle_load_state.store(1, Ordering::SeqCst);
TRASHEDZLE.store(0, Ordering::Relaxed);
RESETNEEDED.store(0, Ordering::Relaxed);
crate::ported::utils::errflag.store(0, Ordering::Relaxed);
let status = super::dispatch_action_command("nosuchcmd_zz_framing_probe", &[], 63);
let trashed = TRASHEDZLE.load(Ordering::Relaxed);
let reset = RESETNEEDED.load(Ordering::Relaxed);
let still_active = zleactive.load(Ordering::Relaxed);
zleactive.store(saved.0, Ordering::Relaxed);
zle_load_state.store(saved.1, Ordering::SeqCst);
TRASHEDZLE.store(saved.2, Ordering::Relaxed);
RESETNEEDED.store(saved.3, Ordering::Relaxed);
crate::ported::utils::errflag.store(0, Ordering::Relaxed);
unsafe {
libc::dup2(saved_stderr, 2);
libc::close(saved_stderr);
libc::close(slave);
libc::close(master);
}
assert_eq!(status, 127, "c:908 — a name that resolves nowhere is 127");
assert_eq!(
trashed, 0,
"trashzle ran: the diagnostic moved the cursor off the command \
line's row (Src/Zle/zle_main.c:2071 is false in C's forked child)"
);
assert_eq!(
reset, 0,
"resetneeded was raised: the next zrefresh repaints the prompt \
BELOW the diagnostic, which zsh never does here"
);
assert_eq!(
still_active, 1,
"the entersubsh stand-in must restore zleactive when it drops"
);
}
}
#[cfg(test)]
mod zstyle_bool_tests {
use super::{empty_ops, zstyle_T, zstyle_t};
const CTX: &str = ":completion:zstyle-bool-probe:zstyle-bool-probe:";
fn set_style(value: &str) {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&[CTX.to_string(), "boolprobe".to_string(), value.to_string()],
&empty_ops(),
0,
);
}
fn del_style() {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-d".to_string(), CTX.to_string(), "boolprobe".to_string()],
&empty_ops(),
0,
);
}
#[test]
fn value_decides_the_exit_not_mere_definition() {
let _g = crate::test_util::global_state_lock();
del_style();
assert_eq!(zstyle_t(CTX, "boolprobe"), 2, "-t, no pattern matched → 2");
assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T, no pattern matched → 0");
for v in ["true", "yes", "on", "1"] {
del_style();
set_style(v);
assert_eq!(zstyle_t(CTX, "boolprobe"), 0, "-t on `{v}` must be 0");
assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T on `{v}` must be 0");
}
for v in ["maybe", "2", "yes-ish", "-1"] {
del_style();
set_style(v);
assert_eq!(
zstyle_t(CTX, "boolprobe"),
1,
"-t on `{v}` must be 1 — only true/yes/on/1 are true"
);
assert_eq!(
zstyle_T(CTX, "boolprobe"),
1,
"-T on `{v}` must be 1 — a set-but-not-true value is FALSE"
);
}
del_style();
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&[CTX.to_string(), "boolprobe".to_string()],
&empty_ops(),
0,
);
assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T on a valueless style → 0");
for v in ["false", "no", "off", "0"] {
del_style();
set_style(v);
assert_eq!(
zstyle_t(CTX, "boolprobe"),
1,
"-t on `{v}` must be 1, not 0 — the style is OFF"
);
assert_eq!(
zstyle_T(CTX, "boolprobe"),
1,
"-T on `{v}` must be 1, not 0 — the style is OFF"
);
assert_eq!(
crate::ported::modules::zutil::testforstyle(CTX, "boolprobe"),
0,
"`testforstyle` reports `{v}` as TRUE — it is zstyle -q's \
primitive (zutil.c:465/749-756) and never reads the value"
);
}
del_style();
}
}
pub fn shfunc_names_with_prefix(prefix: &str) -> Vec<String> {
let Ok(tab) = crate::ported::hashtable::shfunctab_lock().read() else {
return Vec::new();
};
tab.iter()
.filter(|(_, f)| (f.node.flags & crate::ported::zsh_h::DISABLED) == 0)
.filter(|(k, _)| k.starts_with(prefix))
.map(|(k, _)| k.clone())
.collect()
}
pub fn assoc_get(name: &str, key: &str) -> Option<String> {
let keys = crate::ported::params::gethkparam(name).unwrap_or_default();
if !keys.is_empty() {
let vals = crate::ported::params::gethparam(name).unwrap_or_default();
return keys
.iter()
.position(|k| k == key)
.and_then(|i| vals.get(i).cloned());
}
crate::ported::params::getaparam(name)
.unwrap_or_default()
.chunks(2)
.find(|kv| kv.first().map(|k| k == key).unwrap_or(false))
.and_then(|kv| kv.get(1).cloned())
}
pub fn dequote_q(s: &str) -> String {
use std::sync::atomic::Ordering;
let one = *crate::ported::utils::noerrs_lock().lock().unwrap();
let oef = crate::ported::utils::errflag.load(Ordering::Relaxed);
*crate::ported::utils::noerrs_lock().lock().unwrap() = 1;
let parsed = crate::ported::lex::parse_subst_string(s);
*crate::ported::utils::noerrs_lock().lock().unwrap() = one;
let int_bit =
crate::ported::utils::errflag.load(Ordering::Relaxed) & crate::ported::zsh_h::ERRFLAG_INT;
crate::ported::utils::errflag.store(oef | int_bit, Ordering::Relaxed);
match parsed {
Ok(mut r) => {
crate::ported::glob::remnulargs(&mut r); crate::ported::lex::untokenize(&r) }
Err(_) => s.to_string(),
}
}
pub fn tilde_expand(spec: &str) -> Option<String> {
let rest = spec.strip_prefix('~')?;
crate::ported::subst::filesubstr(&format!("\u{e198}{}", rest), false)
}
pub struct LocalOptions {
opts: std::collections::HashMap<String, bool>,
emulation: i32,
emulation_bits: i32,
fully: bool,
dash: bool,
}
impl LocalOptions {
pub fn enter() -> Self {
use std::sync::atomic::Ordering::Relaxed;
Self {
opts: crate::ported::options::opt_state_snapshot(),
emulation: crate::ported::options::emulation.load(Relaxed),
emulation_bits: crate::ported::options::EMULATION.load(Relaxed),
fully: crate::ported::options::FULLY_EMULATING.load(Relaxed),
dash: crate::extensions::dash_mode::dash_strict(),
}
}
}
impl Drop for LocalOptions {
fn drop(&mut self) {
use std::sync::atomic::Ordering::Relaxed;
crate::ported::options::opt_state_restore(std::mem::take(&mut self.opts));
crate::ported::options::emulation.store(self.emulation, Relaxed);
crate::ported::options::EMULATION.store(self.emulation_bits, Relaxed);
crate::ported::options::FULLY_EMULATING.store(self.fully, Relaxed);
crate::extensions::dash_mode::set_dash_strict(self.dash);
}
}
pub fn split_z(s: &str) -> Vec<String> {
crate::ported::hist::bufferwords(s, None, crate::ported::zsh_h::LEXFLAGS_ACTIVE).0
}
pub fn assoc_key_count_with_prefix(name: &str, prefix: &str) -> usize {
let keys = crate::ported::params::gethkparam(name).unwrap_or_default();
if !keys.is_empty() {
return keys.iter().filter(|k| k.starts_with(prefix)).count();
}
crate::ported::params::getaparam(name)
.unwrap_or_default()
.chunks(2)
.filter_map(|kv| kv.first())
.filter(|k| k.starts_with(prefix))
.count()
}
#[cfg(test)]
mod assoc_accessor_tests {
use super::{assoc_get, assoc_key_count_with_prefix};
use crate::ported::params::{getaparam, sethparam, unsetparam};
#[test]
fn reads_a_pm_hashed_parameter_that_getaparam_cannot_see() {
let _g = crate::test_util::global_state_lock();
const NAME: &str = "zzq_shared_assoc_probe";
sethparam(
NAME,
vec![
"alpha".to_string(),
"one".to_string(),
"alpine".to_string(),
"two".to_string(),
"beta".to_string(),
"three".to_string(),
],
);
assert!(
getaparam(NAME).is_none(),
"premise broken: getaparam now sees a PM_HASHED param, so this \
test no longer guards anything"
);
assert_eq!(assoc_get(NAME, "alpha"), Some("one".to_string()));
assert_eq!(assoc_get(NAME, "beta"), Some("three".to_string()));
assert_eq!(assoc_get(NAME, "gamma"), None);
assert_eq!(assoc_key_count_with_prefix(NAME, "al"), 2);
assert_eq!(assoc_key_count_with_prefix(NAME, "b"), 1);
assert_eq!(assoc_key_count_with_prefix(NAME, "zz"), 0);
unsetparam(NAME);
}
}
pub fn plus_builtins(name: &str) -> bool {
if name.is_empty() {
return false;
}
match crate::ported::modules::parameter::getpmbuiltin(std::ptr::null_mut(), name) {
Some(pm) => (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) == 0,
None => false,
}
}
pub fn plus_commands(name: &str) -> bool {
crate::vm_helper::mark_module_param_used("commands");
crate::ported::modules::parameter::getpmcommand(std::ptr::null_mut(), name)
.map(|pm| (pm.node.flags & crate::ported::zsh_h::PM_UNSET as i32) == 0)
.unwrap_or(false)
}
#[cfg(test)]
mod plus_builtins_tests {
use super::plus_builtins;
use crate::ported::builtin::{createbuiltintable, BUILTINS_DISABLED};
#[test]
fn disabled_builtin_is_unset_even_though_the_raw_table_holds_it() {
let _g = crate::test_util::global_state_lock();
const NAME: &str = "print";
assert!(plus_builtins(NAME), "premise: `print` starts enabled");
let re_enable = {
let mut set = BUILTINS_DISABLED.lock().unwrap();
set.insert(NAME.to_string())
};
let while_disabled = plus_builtins(NAME);
let raw_table_still_has_it = createbuiltintable().contains_key(NAME);
if re_enable {
BUILTINS_DISABLED.lock().unwrap().remove(NAME);
}
assert!(
raw_table_still_has_it,
"premise: the static table keeps the node when a builtin is disabled"
);
assert!(
!while_disabled,
"`disable print` must make `$+builtins[print]` 0 (c:parameter.c:784-792)"
);
assert!(plus_builtins(NAME), "`print` restored after the probe");
}
#[test]
fn empty_name_is_not_a_builtin() {
let _g = crate::test_util::global_state_lock();
assert!(!plus_builtins(""));
}
}
pub fn plus_parameters(name: &str) -> bool {
if name.is_empty() {
return false;
}
crate::ported::params::paramtab()
.read()
.map(|t| match t.get(name) {
Some(pm) => (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) == 0,
None => false,
})
.unwrap_or(false)
}
pub fn parameter_count_with_prefix(prefix: &str) -> usize {
crate::ported::params::paramtab()
.read()
.map(|t| {
t.iter()
.filter(|(k, pm)| {
k.starts_with(prefix)
&& (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) == 0
})
.count()
})
.unwrap_or(0)
}
#[cfg(test)]
mod plus_parameters_tests {
use super::{parameter_count_with_prefix, plus_parameters};
use crate::ported::params::{paramtab, setsparam, unsetparam};
#[test]
fn unset_parameter_is_absent_from_both_the_test_and_the_count() {
let _g = crate::test_util::global_state_lock();
const NAME: &str = "ZZQ_SHARED_PARAM_PROBE";
setsparam(NAME, "value");
assert!(plus_parameters(NAME), "premise: the name starts set");
assert_eq!(parameter_count_with_prefix(NAME), 1);
unsetparam(NAME);
let node_still_there = paramtab()
.read()
.map(|t| t.get(NAME).is_some())
.unwrap_or(false);
assert!(
!plus_parameters(NAME),
"`$+parameters[{}]` must be 0 once unset (c:parameter.c:114-115); \
node still in paramtab: {}",
NAME,
node_still_there
);
assert_eq!(
parameter_count_with_prefix(NAME),
0,
"the `(I)` scan skips PM_UNSET nodes (c:parameter.c:138-139)"
);
}
#[test]
fn empty_name_is_not_a_parameter() {
let _g = crate::test_util::global_state_lock();
assert!(!plus_parameters(""));
}
}
pub fn plus_functions(name: &str) -> bool {
if name.is_empty() {
return false;
}
let in_shfunctab =
match crate::ported::modules::parameter::getpmfunction(std::ptr::null_mut(), name) {
Some(pm) => (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) == 0,
None => false,
};
if in_shfunctab {
return true;
}
crate::compsys::router::try_rust_dispatch(name).is_some()
|| crate::extensions::plugin_host::compfn_override(name).is_some()
}
#[cfg(test)]
mod plus_functions_tests {
use super::plus_functions;
#[test]
fn a_routed_port_with_no_definition_file_counts_as_a_function() {
let _g = crate::test_util::global_state_lock();
let routed = crate::compsys::router::try_rust_dispatch("_shadow").is_some();
let in_table = crate::ported::utils::getshfunc("_shadow").is_some();
if !routed {
return;
}
assert!(
plus_functions("_shadow"),
"router serves `_shadow` (shfunctab node: {in_table}) so \
`$+functions[_shadow]` must not read 0"
);
}
#[test]
fn unknown_name_is_not_a_function() {
let _g = crate::test_util::global_state_lock();
assert!(!plus_functions("_zzq_no_such_completer_xyz"));
assert!(!plus_functions(""));
}
}
pub fn caller_is_prefix() -> bool {
crate::ported::modules::parameter::funcstackgetfn(std::ptr::null_mut())
.get(1)
.map(|n| n == "_prefix")
.unwrap_or(false)
}
pub fn expansion_description_args(sort: &str, tag: &str, descr: &str, word: &str) -> Vec<String> {
let mut args: Vec<String> = Vec::new();
if sort != "menu" {
args.push("-V".to_string());
}
args.push(tag.to_string());
args.push("expl".to_string());
args.push(descr.to_string());
args.push(format!("o:{}", word));
args
}
pub fn expansion_partition_argv(
expl: &[String],
pref: Option<&str>,
suf: &str,
array: &str,
) -> Vec<String> {
let mut argv: Vec<String> = expl.to_vec();
if let Some(pref) = pref {
argv.push("-fW".to_string());
argv.push(pref.to_string());
}
argv.extend([
"-UQ".to_string(),
"-qS".to_string(),
suf.to_string(),
"-a".to_string(),
array.to_string(),
]);
argv
}
pub fn right_pad_or_truncate(s: &str, n: usize) -> String {
let len = s.chars().count();
if len >= n {
s.chars().take(n).collect()
} else {
format!("{}{}", s, " ".repeat(n - len))
}
}
pub fn strip_shortest_space_delimited_suffix(s: &str) -> String {
if !s.ends_with(' ') {
return s.to_string();
}
let bytes = s.as_bytes();
if bytes.len() < 2 {
return s.to_string();
}
match bytes[..bytes.len() - 1].iter().rposition(|&b| b == b' ') {
Some(i) => s[..i].to_string(),
None => s.to_string(),
}
}
#[cfg(test)]
mod strip_shortest_space_delimited_suffix_tests {
use super::strip_shortest_space_delimited_suffix as strip;
#[test]
fn matches_zsh_parameter_expansion_on_every_measured_shape() {
assert_eq!(strip(" a b c "), " a b");
assert_eq!(strip(" a b "), " a");
assert_eq!(strip(" a b "), " a b");
assert_eq!(strip("a b "), "a");
assert_eq!(strip(" abc "), "");
}
#[test]
fn a_value_without_a_trailing_space_is_unchanged() {
assert_eq!(strip(" a b"), " a b");
assert_eq!(strip("abc"), "abc");
assert_eq!(strip(""), "");
}
#[test]
fn a_lone_space_is_unchanged() {
assert_eq!(strip(" "), " ");
}
#[test]
fn strip_then_append_leaves_a_single_space_between_specs() {
let after_strip = strip(" tag1 tag2 ");
assert_eq!(format!("{} {} ", after_strip, "tag3"), " tag1 tag3 ");
}
}
#[cfg(test)]
mod slash_action_tests {
use super::*;
#[test]
fn a_slash_action_reports_the_exec_errno() {
let _g = crate::test_util::global_state_lock();
assert_eq!(direct_exec_errno("//"), libc::EACCES);
assert_eq!(direct_exec_errno("/"), libc::EACCES);
assert_eq!(direct_exec_errno("/nonexistent-zshrs-dir/x"), libc::ENOENT);
assert_eq!(direct_exec_errno("/bin/sh"), 0);
assert_eq!(dispatch_action_command("//", &["URL:".to_string()], 0), 126);
assert_eq!(dispatch_action_command("/nonexistent-zshrs-dir/x", &[], 0), 127);
}
}