use crate::ported::hashtable::{removeshfuncnode, shfunctab_lock};
use crate::ported::modules::parameter::setfunction;
use crate::ported::params::{getaparam, getiparam, getsparam, setaparam, setiparam, setsparam};
use crate::ported::utils::getshfunc;
const STACK_PARAM: &str = ".shadow.stack";
const DEPTH_PARAM: &str = ".shadow.depth";
const SEP: &str = "--";
fn function_defined(name: &str) -> bool {
getshfunc(name).is_some()
}
fn builtin_defined(name: &str) -> bool {
crate::ported::builtin::createbuiltintable().contains_key(name)
}
fn functions_copy(src: &str, dst: &str) -> bool {
let mut tab = match shfunctab_lock().write() {
Ok(t) => t,
Err(_) => return false,
};
match tab.get_including_disabled(src).cloned() {
Some(mut copy) => {
copy.node.nam = dst.to_string();
tab.add(copy);
true
}
None => false,
}
}
fn unfunction(name: &str) {
let _ = removeshfuncnode(name);
}
fn default_suffix(depth: i64) -> String {
let fs = crate::ported::modules::parameter::funcstackgetfn(std::ptr::null_mut());
let ft = crate::ported::modules::parameter::functracegetfn(std::ptr::null_mut());
let f2 = fs.get(1).cloned().unwrap_or_default();
let t2 = ft.get(1).cloned().unwrap_or_default();
format!("{}:{}:{}", f2, t2, depth + 1)
}
fn parse_opts(args: &[String], default: String) -> (String, Vec<String>) {
if args.is_empty() {
return (default, Vec::new());
}
if args[0] == "-s" {
if args.len() >= 2 {
return (args[1].clone(), args[2..].to_vec());
}
return (default, args[1..].to_vec());
}
if let Some(glued) = args[0].strip_prefix("-s") {
if !glued.is_empty() {
return (glued.to_string(), args[1..].to_vec());
}
}
(default, args.to_vec())
}
pub fn _shadow(args: &[String]) -> i32 {
let depth = getiparam(DEPTH_PARAM);
let (suffix, fnames_in) = parse_opts(args, default_suffix(depth));
let mut fnames: Vec<String> = Vec::with_capacity(fnames_in.len());
for fname in &fnames_in {
let shadowname = format!("{}@{}", fname, suffix);
if function_defined(&shadowname) {
continue;
} else if function_defined(fname) {
functions_copy(fname, &shadowname);
fnames.push(format!("f@{}", fname));
} else if builtin_defined(fname) {
setfunction(&shadowname, format!("builtin {} \"$@\"", fname), 0);
fnames.push(format!("b@{}", fname));
} else {
setfunction(&shadowname, format!("command {} \"$@\"", fname), 0);
fnames.push(format!("c@{}", fname));
}
}
if getsparam("REPLY").unwrap_or_default().is_empty() {
let _ = setsparam("REPLY", &suffix);
}
let old = getaparam(STACK_PARAM).unwrap_or_default();
let mut new_stack: Vec<String> = Vec::with_capacity(2 + fnames.len() + old.len());
new_stack.push(suffix);
new_stack.extend(fnames);
new_stack.push(SEP.to_string());
new_stack.extend(old);
setaparam(STACK_PARAM, new_stack);
setiparam(DEPTH_PARAM, depth + 1);
0
}
pub fn _unshadow() -> i32 {
let mut stack = getaparam(STACK_PARAM).unwrap_or_default();
let fsfx = stack.first().cloned().unwrap_or_default();
if fsfx.is_empty() {
return 1;
}
stack.remove(0);
while stack.first().map(|s| s.as_str()) != Some(SEP) {
let entry = match stack.first().cloned() {
Some(e) => e,
None => break,
};
let fname = strip_marker(&entry);
let shadowname = format!("{}@{}", fname, fsfx);
if function_defined(&fname) {
unfunction(&fname);
}
if entry.starts_with("f@") {
functions_copy(&shadowname, &fname);
unfunction(&shadowname);
} else if entry.starts_with("b@") || entry.starts_with("c@") {
unfunction(&shadowname);
}
stack.remove(0);
}
if getsparam("REPLY").unwrap_or_default().is_empty() {
let _ = setsparam("REPLY", &fsfx);
}
if stack.first().map(|s| s.as_str()) == Some(SEP) {
stack.remove(0);
}
setaparam(STACK_PARAM, stack);
setiparam(DEPTH_PARAM, getiparam(DEPTH_PARAM) - 1);
0
}
fn strip_marker(entry: &str) -> String {
let mut chars = entry.chars();
match (chars.next(), chars.next()) {
(Some(_), Some('@')) => chars.collect(),
_ => entry.to_string(),
}
}
#[cfg(test)]
pub fn reset_shadow_state() {
setaparam(STACK_PARAM, Vec::new());
setiparam(DEPTH_PARAM, 0);
let _ = setsparam("REPLY", "");
}
#[cfg(test)]
pub fn current_backup_name(fname: &str) -> Option<String> {
let stack = getaparam(STACK_PARAM).unwrap_or_default();
let suffix = stack.first()?.clone();
for entry in stack.iter().skip(1) {
if entry == SEP {
break;
}
if strip_marker(entry) == fname {
return Some(format!("{}@{}", fname, suffix));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::zsh_h::{hashnode, shfunc};
fn make_shfunc(name: &str, body: &str) -> shfunc {
shfunc {
node: hashnode {
nam: name.to_string(),
next: None,
flags: 0,
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: Some(body.to_string()),
}
}
fn body_of(name: &str) -> Option<String> {
let tab = shfunctab_lock().read().unwrap();
tab.get_including_disabled(name)
.and_then(|f| f.body.clone())
}
#[test]
fn shadow_existing_function_creates_f_marker_backup() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("compadd");
tab.add(make_shfunc("compadd", "echo original"));
}
assert_eq!(_shadow(&["compadd".to_string()]), 0);
let backup = current_backup_name("compadd").unwrap();
assert_eq!(body_of(&backup).as_deref(), Some("echo original"));
let stack = getaparam(STACK_PARAM).unwrap();
assert!(stack.iter().any(|e| e == "f@compadd"));
reset_shadow_state();
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("compadd");
tab.remove(&backup);
}
#[test]
fn shadow_builtin_installs_wrapper_with_b_marker() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("compadd");
}
assert!(builtin_defined("compadd"), "compadd must be a builtin");
let _ = _shadow(&["-s".to_string(), "_approximate".to_string(), "compadd".to_string()]);
let backup = current_backup_name("compadd").unwrap();
assert_eq!(backup, "compadd@_approximate");
assert_eq!(body_of(&backup).as_deref(), Some("builtin compadd \"$@\""));
let stack = getaparam(STACK_PARAM).unwrap();
assert!(stack.iter().any(|e| e == "b@compadd"));
let _ = _unshadow();
assert!(body_of(&backup).is_none());
}
#[test]
fn shadow_command_installs_wrapper_with_c_marker() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
let name = "zz_not_a_builtin_or_fn";
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove(name);
}
assert!(!function_defined(name));
assert!(!builtin_defined(name));
let _ = _shadow(&["-s".to_string(), "sfx".to_string(), name.to_string()]);
let backup = current_backup_name(name).unwrap();
assert_eq!(body_of(&backup).as_deref(), Some(format!("command {} \"$@\"", name).as_str()));
let stack = getaparam(STACK_PARAM).unwrap();
assert!(stack.iter().any(|e| e == &format!("c@{}", name)));
let _ = _unshadow();
assert!(body_of(&backup).is_none());
}
#[test]
fn reshadow_same_suffix_is_ignored() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("f");
tab.add(make_shfunc("f", "v1"));
}
let _ = _shadow(&["-s".to_string(), "sfx".to_string(), "f".to_string()]);
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc("f", "v2"));
}
let _ = _shadow(&["-s".to_string(), "sfx".to_string(), "f".to_string()]);
assert_eq!(body_of("f@sfx").as_deref(), Some("v1"));
reset_shadow_state();
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("f");
tab.remove("f@sfx");
}
#[test]
fn unshadow_restores_original_definition() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("myfn");
tab.add(make_shfunc("myfn", "echo original-body"));
}
let _ = _shadow(&["myfn".to_string()]);
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc("myfn", "echo override-body"));
}
assert_eq!(body_of("myfn").as_deref(), Some("echo override-body"));
assert_eq!(_unshadow(), 0);
assert_eq!(body_of("myfn").as_deref(), Some("echo original-body"));
reset_shadow_state();
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("myfn");
}
#[test]
fn unshadow_removes_wrapper_when_no_prior_function() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
let name = "zz_ghost_cmd";
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove(name);
}
let _ = _shadow(&[name.to_string()]);
let backup = current_backup_name(name).unwrap();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc(name, "echo created"));
}
assert_eq!(_unshadow(), 0);
assert!(!function_defined(name));
assert!(body_of(&backup).is_none());
}
#[test]
fn unshadow_on_empty_stack_returns_one() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
assert_eq!(_unshadow(), 1);
}
#[test]
fn shadow_and_unshadow_balances_depth() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc("a", ""));
tab.add(make_shfunc("b", ""));
}
let _ = _shadow(&["a".to_string(), "b".to_string()]);
assert_eq!(getiparam(DEPTH_PARAM), 1);
let _ = _unshadow();
assert_eq!(getiparam(DEPTH_PARAM), 0);
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("a");
tab.remove("b");
}
#[test]
fn explicit_suffix_via_dash_s() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc("targetfn", ""));
}
let _ = _shadow(&[
"-s".to_string(),
"my-suffix".to_string(),
"targetfn".to_string(),
]);
assert_eq!(current_backup_name("targetfn").unwrap(), "targetfn@my-suffix");
let _ = _unshadow();
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("targetfn");
tab.remove("targetfn@my-suffix");
}
#[test]
fn multiple_frames_stack_lifo() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("x");
tab.add(make_shfunc("x", "v1"));
}
let _ = _shadow(&["-s".to_string(), "s1".to_string(), "x".to_string()]);
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc("x", "v2"));
}
let _ = _shadow(&["-s".to_string(), "s2".to_string(), "x".to_string()]);
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(make_shfunc("x", "v3"));
}
assert_eq!(_unshadow(), 0);
assert_eq!(body_of("x").as_deref(), Some("v2"));
assert_eq!(_unshadow(), 0);
assert_eq!(body_of("x").as_deref(), Some("v1"));
reset_shadow_state();
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("x");
}
#[test]
fn stack_layout_uses_real_markers_and_separator() {
let _g = crate::test_util::global_state_lock();
reset_shadow_state();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("fn1");
tab.add(make_shfunc("fn1", ""));
}
let _ = _shadow(&["-s".to_string(), "frame1".to_string(), "fn1".to_string()]);
let stack = getaparam(STACK_PARAM).unwrap_or_default();
assert_eq!(stack, vec!["frame1", "f@fn1", "--"]);
let _ = _unshadow();
let mut tab = shfunctab_lock().write().unwrap();
tab.remove("fn1");
}
#[test]
fn strip_marker_matches_hash_question_at() {
assert_eq!(strip_marker("f@compadd"), "compadd");
assert_eq!(strip_marker("b@zstyle"), "zstyle");
assert_eq!(strip_marker("c@ls"), "ls");
assert_eq!(strip_marker("plain"), "plain");
}
}