use crate::compsys::ported::_description::_description;
use crate::ported::exec_hooks::dispatch_function_call;
use crate::ported::modules::zutil::{lookupstyle, testforstyle};
use crate::ported::params::{getaparam, getiparam, getsparam, setaparam};
use crate::ported::zle::compcore::get_compstate_str;
use crate::ported::zle::complete::bin_compadd;
use crate::ported::zsh_h::{options, MAX_OPS};
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
pub fn _expand() -> i32 {
if getiparam("_matcher_num") > 1 {
return 1;
}
let iprefix = getsparam("IPREFIX").unwrap_or_default();
let prefix = getsparam("PREFIX").unwrap_or_default();
let suffix = getsparam("SUFFIX").unwrap_or_default();
let isuffix = getsparam("ISUFFIX").unwrap_or_default();
let word = format!("{}{}{}{}", iprefix, prefix, suffix, isuffix);
let curcontext = getsparam("curcontext").unwrap_or_default();
let ctx = format!(":completion:{}:", curcontext);
let mut exp: Vec<String> = vec![word.clone()];
let subst_on = lookupstyle(&ctx, "substitute")
.first()
.map(|v| !matches!(v.as_str(), "no" | "false" | "0" | "off"))
.unwrap_or(true);
if subst_on && (word.contains('$') || word.contains('~') || word.contains('=')) {
if let Some(expanded) = expand_substitutions(&word) {
if expanded != word {
exp = vec![expanded];
}
}
}
let glob_on = lookupstyle(&ctx, "glob")
.first()
.map(|v| !matches!(v.as_str(), "no" | "false" | "0" | "off"))
.unwrap_or(true);
if glob_on && word.chars().any(|c| matches!(c, '*' | '?' | '[')) {
if let Ok(paths) = glob_match(&word) {
if !paths.is_empty() {
exp = paths;
}
}
}
if exp.len() == 1 && exp[0] == word {
return 1;
}
setaparam("exp", exp);
let _ = _description(&[
"-V".to_string(),
"expansions".to_string(),
"expl".to_string(),
"expansions".to_string(),
format!("o:{}", word),
]);
let add_space = testforstyle(&ctx, "add-space") == 0;
let suf: Vec<String> = if add_space {
vec!["-qS".to_string(), " ".to_string()]
} else {
vec!["-qS".to_string(), "".to_string()]
};
let insert = get_compstate_str("insert").unwrap_or_default();
let expl = getaparam("expl").unwrap_or_default();
let mut compadd_argv: Vec<String> = expl;
compadd_argv.push("-UQ".to_string());
compadd_argv.extend(suf);
compadd_argv.push("-a".to_string());
compadd_argv.push("exp".to_string());
let r = bin_compadd("compadd", &compadd_argv, &make_ops(), 0);
let _ = insert;
let _ = dispatch_function_call;
r
}
fn expand_substitutions(word: &str) -> Option<String> {
let mut out = word.to_string();
let initial = out.clone();
if out.starts_with("~/") {
if let Ok(home) = std::env::var("HOME") {
out = format!("{}{}", home, &out[1..]);
}
} else if out.starts_with('~') && !out.starts_with("~[") {
let end = out.find('/').unwrap_or(out.len());
let user = &out[1..end];
if !user.is_empty() {
let userdirs = crate::ported::params::getaparam("userdirs").unwrap_or_default();
if let Some(home) = userdirs.chunks(2).find_map(|kv| {
if kv.first().map(|k| k == user).unwrap_or(false) {
kv.get(1).cloned()
} else {
None
}
}) {
let rest = &out[end..];
out = format!("{}{}", home, rest);
}
}
}
let mut buf = String::with_capacity(out.len());
let bytes = out.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' && i + 1 < bytes.len() {
let (name, end) = if bytes[i + 1] == b'{' {
if let Some(close) = out[i + 2..].find('}') {
let n = &out[i + 2..i + 2 + close];
(n.to_string(), i + 3 + close)
} else {
buf.push('$');
i += 1;
continue;
}
} else {
let mut j = i + 1;
while j < bytes.len()
&& (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_')
{
j += 1;
}
if j == i + 1 {
buf.push('$');
i += 1;
continue;
}
(out[i + 1..j].to_string(), j)
};
if let Ok(v) = std::env::var(&name) {
buf.push_str(&v);
}
i = end;
} else {
buf.push(bytes[i] as char);
i += 1;
}
}
out = buf;
if out == initial {
None
} else {
Some(out)
}
}
fn glob_match(pat: &str) -> Result<Vec<String>, ()> {
use crate::ported::pattern::{patcompile, pattry};
let (dir, name_pat) = match pat.rfind('/') {
Some(i) => (pat[..=i].to_string(), pat[i + 1..].to_string()),
None => (".".to_string(), pat.to_string()),
};
let scan_dir = if dir.is_empty() { "." } else { &dir };
let prog = patcompile(&name_pat, 0, None).ok_or(())?;
let entries = std::fs::read_dir(std::path::Path::new(scan_dir)).map_err(|_| ())?;
let mut out: Vec<String> = Vec::new();
for ent in entries.flatten() {
let name = ent.file_name().to_string_lossy().into_owned();
if name.starts_with('.') && !name_pat.starts_with('.') {
continue;
}
if pattry(&prog, &name) {
let full = if dir == "." {
name
} else {
format!("{}{}", dir, name)
};
out.push(full);
}
}
out.sort();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::params::{setiparam, setsparam};
#[test]
fn matcher_num_gt_one_returns_one() {
let _g = crate::test_util::global_state_lock();
setiparam("_matcher_num", 5);
assert_eq!(_expand(), 1);
setiparam("_matcher_num", 0);
}
#[test]
fn plain_word_no_substitution_returns_one() {
let _g = crate::test_util::global_state_lock();
setiparam("_matcher_num", 1);
let _ = setsparam("PREFIX", "plain");
let _ = setsparam("SUFFIX", "");
let _ = setsparam("IPREFIX", "");
let _ = setsparam("ISUFFIX", "");
assert_eq!(_expand(), 1);
}
}