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_LOCAL;
use std::sync::atomic::Ordering;
let cur = locallevel.load(Ordering::Relaxed); if cur == 0 {
return;
}
for name in names {
let needs_shadow = paramtab()
.read()
.ok()
.and_then(|t| t.get(*name).map(|pm| pm.level < cur))
.unwrap_or(true);
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;
}
}
}
}
pub struct LocalScope {
saved: Vec<(String, Option<Box<crate::ported::zsh_h::param>>)>,
}
impl LocalScope {
pub fn declare(names: &[&str], kind: u32) -> Self {
let mut scope = LocalScope { saved: Vec::new() };
scope.also(names, kind);
scope
}
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()));
}
}
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()));
}
}
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);
}
}
}
}
}
}
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;
}
}
}
}
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 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();
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,
}
}
}
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 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 ctx = crate::ported::exec::EvalContextFrame::push("eval");
let lastval = crate::ported::exec::execute_script(comp).unwrap_or(1);
drop(ctx);
drop(fstack); crate::ported::utils::set_scriptname(oscriptname); lastval }
#[cfg(test)]
mod lineno_scope_tests {
use super::*;
use crate::ported::lex::{lineno, set_lineno};
#[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);
}
}