use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::Instant;
pub const COMP_OPTIONS: &[&str] = &[
"bareglobqual",
"extendedglob",
"glob",
"multibyte",
"multifuncdef",
"nullglob",
"rcexpandparam",
"unset",
"NO_allexport",
"NO_aliases",
"NO_autonamedirs",
"NO_cshnullglob",
"NO_cshjunkiequotes",
"NO_errexit",
"NO_errreturn",
"NO_globassign",
"NO_globsubst",
"NO_histsubstpattern",
"NO_ignorebraces",
"NO_ignoreclosebraces",
"NO_kshglob",
"NO_ksharrays",
"NO_kshtypeset",
"NO_markdirs",
"NO_octalzeroes",
"NO_posixbuiltins",
"NO_posixidentifiers",
"NO_shwordsplit",
"NO_shglob",
"NO_typesettounset",
"NO_warnnestedvar",
"NO_warncreateglobal",
];
pub const COMP_SETUP_EVAL: &str = concat!(
"local -A _comp_caller_options;\n",
"_comp_caller_options=(${(kv)options[@]});\n",
"setopt localoptions localtraps localpatterns ${_comp_options[@]};\n",
"local IFS=$' \\t\\r\\n\\0';\n",
"builtin enable -p \\| \\~ \\( \\? \\* \\[ \\< \\^ \\# 2>&-;\n",
"exec </dev/null;\n",
"trap - ZERR;\n",
"local -a reply;\n",
"local REPLY;\n",
"local REPORTTIME;\n",
"unset REPORTTIME"
);
pub const STANDARD_COMPLETE_WIDGETS: &[&str] = &[
"complete-word",
"delete-char-or-list",
"expand-or-complete",
"expand-or-complete-prefix",
"list-choices",
"menu-complete",
"menu-expand-or-complete",
"reverse-menu-complete",
];
pub fn init_comp_funcs_arrays() {
crate::ported::params::setaparam("compprefuncs", Vec::new());
crate::ported::params::setaparam("comppostfuncs", Vec::new());
}
fn declare_global(name: &str, kind: u32, attrs: u32) {
use crate::ported::params::{paramtab, setaparam, sethparam, setsparam};
use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED};
let exists = paramtab()
.read()
.ok()
.map(|t| t.contains_key(name))
.unwrap_or(false);
if !exists {
if kind & PM_HASHED != 0 {
sethparam(name, Vec::new());
} else if kind & PM_ARRAY != 0 {
setaparam(name, Vec::new());
} else {
let _ = setsparam(name, "");
}
}
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(name) {
pm.node.flags |= attrs as i32;
}
}
}
pub fn declare_compinit_globals(dumpfile: Option<&str>) {
use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED, PM_HIDEVAL, PM_UNIQUE};
for name in [
"_comps",
"_services",
"_patcomps",
"_postpatcomps",
"_compautos",
"_lastcomp",
] {
declare_global(name, PM_HASHED, PM_HIDEVAL);
}
match dumpfile {
Some(f) if !f.is_empty() => {
let _ = crate::ported::params::setsparam("_comp_dumpfile", f); }
_ => {
if crate::ported::params::getsparam("_comp_dumpfile")
.map(|s| s.is_empty())
.unwrap_or(true)
{
let _ = crate::ported::params::setsparam(
"_comp_dumpfile",
&default_dumpfile_path().to_string_lossy(),
); }
}
}
declare_global("_comp_options", PM_ARRAY, PM_HIDEVAL);
crate::ported::params::setaparam(
"_comp_options",
COMP_OPTIONS.iter().map(|s| s.to_string()).collect(),
);
declare_global("_comp_setup", 0, PM_HIDEVAL);
let _ = crate::ported::params::setsparam("_comp_setup", COMP_SETUP_EVAL);
init_comp_funcs_arrays();
declare_global("_comp_assocs", PM_ARRAY, PM_UNIQUE);
}
pub fn register_autoload_stubs<I, S>(names: I) -> usize
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
use crate::ported::zsh_h::{PM_UNALIASED, PM_UNDEFINED, PM_ZSHSTORED};
let flags = (PM_UNDEFINED | PM_UNALIASED | PM_ZSHSTORED) as i32;
let mut added = 0usize;
let names = names.into_iter();
let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() else {
return 0;
};
tab.reserve(names.size_hint().0);
for name in names {
let name = name.as_ref();
if name.is_empty() || tab.contains_key(name) {
continue;
}
let mut stub = crate::ported::hashtable::shfunc_autoload(name);
stub.node.flags = flags;
tab.add(stub);
added += 1;
}
added
}
pub fn autoload_stub_names(result: &CompInitResult) -> Vec<&str> {
result
.files
.iter()
.filter(|f| matches!(f.def, CompFileDef::CompDef(_) | CompFileDef::Autoload(_)))
.map(|f| f.name.as_str())
.collect()
}
pub fn dump_autoload_names(path: &Path) -> Vec<String> {
let Ok(text) = std::fs::read_to_string(path) else {
return Vec::new();
};
let mut names = Vec::new();
let mut in_autoload = false;
for line in text.lines() {
let mut rest = line.trim();
if !in_autoload {
let Some(tail) = rest.strip_prefix("autoload") else {
continue;
};
if !tail.is_empty() && !tail.starts_with(|c: char| c.is_ascii_whitespace()) {
continue;
}
rest = tail.trim_start();
}
in_autoload = rest.ends_with('\\');
if in_autoload {
rest = rest[..rest.len() - 1].trim_end();
}
for word in rest.split_ascii_whitespace() {
if word.starts_with('-') || word.starts_with('+') {
continue; }
names.push(word.to_string());
}
}
names
}
#[derive(Debug, Default)]
pub struct DumpTables {
pub comps: indexmap::IndexMap<String, String>,
pub services: indexmap::IndexMap<String, String>,
pub patcomps: indexmap::IndexMap<String, String>,
pub postpatcomps: indexmap::IndexMap<String, String>,
pub compautos: indexmap::IndexMap<String, String>,
}
fn split_quoted_words(line: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut started = false;
let mut it = line.chars();
while let Some(c) = it.next() {
match c {
' ' | '\t' => {
if started {
out.push(std::mem::take(&mut cur));
started = false;
}
}
'\'' => {
started = true;
for d in it.by_ref() {
if d == '\'' {
break;
}
cur.push(d);
}
}
'\\' => {
started = true;
if let Some(d) = it.next() {
cur.push(d);
}
}
_ => {
started = true;
cur.push(c);
}
}
}
if started {
out.push(cur);
}
out
}
pub fn dump_assoc_tables(path: &Path) -> Option<DumpTables> {
let text = std::fs::read_to_string(path).ok()?;
let mut tables = DumpTables::default();
let mut open: Option<usize> = None;
for line in text.lines() {
if let Some(idx) = open {
if line.trim_end() == ")" {
open = None;
continue;
}
let words = split_quoted_words(line);
if words.len() >= 2 {
let table = match idx {
0 => &mut tables.comps,
1 => &mut tables.services,
2 => &mut tables.patcomps,
3 => &mut tables.postpatcomps,
_ => &mut tables.compautos,
};
table.insert(words[0].clone(), words[1].clone());
}
continue;
}
open = match line.trim_end() {
"_comps=(" => Some(0),
"_services=(" => Some(1),
"_patcomps=(" => Some(2),
"_postpatcomps=(" => Some(3),
"_compautos=(" => Some(4),
_ => None,
};
}
Some(tables)
}
pub fn default_dumpfile_path() -> PathBuf {
let home = std::env::var("ZDOTDIR")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("HOME").ok())
.unwrap_or_else(|| ".".to_string());
PathBuf::from(home).join(".zcompdump")
}
pub fn install_standard_complete_widgets() -> usize {
let empty_ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let mut count = 0usize;
tracing::debug!(target: "compsys_args", "install_standard_complete_widgets ENTER");
for w in STANDARD_COMPLETE_WIDGETS {
let args = [
w.to_string(),
format!(".{}", w),
"_main_complete".to_string(),
];
let rc_w = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0);
tracing::debug!(target: "compsys_args", widget = %w, rc_w, "zle -C standard widget");
if rc_w == 0 {
count += 1;
}
}
{
let args = [
"menu-select".to_string(),
".menu-select".to_string(),
"_main_complete".to_string(),
];
let rc_w = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0);
tracing::debug!(target: "compsys_args", widget = "menu-select", rc_w, "zle -C standard widget");
if rc_w == 0 {
count += 1;
}
}
count
}
fn load_module_i(name: &str) {
let mut ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'i' as usize] = 1;
let _ = crate::ported::module::bin_zmodload("zmodload", &[name.to_string()], &ops, 0);
}
pub fn touch_funcstack_param() {
load_module_i("zsh/parameter");
}
pub fn maybe_rebind_tab_for_expand() {
load_module_i("zsh/zutil");
let completers = crate::ported::modules::zutil::lookupstyle(":completion:", "completer");
let has_expand = completers.iter().any(|c| c == "_expand");
if !has_expand {
return;
}
let seq = crate::ported::zle::zle_bindings::getkeystring("^i");
let km = crate::ported::zle::zle_keymap::openkeymap("main").or_else(|| {
crate::ported::zle::zle_keymap::default_bindings();
crate::ported::zle::zle_keymap::openkeymap("main")
});
let bound = km
.and_then(|km| crate::ported::zle::zle_keymap::keybind(&km, &seq).0)
.map(|t| t.nam);
if bound.as_deref() != Some("expand-or-complete") {
return;
}
let empty_ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let bk_args = ["^i".to_string(), "complete-word".to_string()];
let _ = crate::ported::zle::zle_keymap::bin_bindkey("bindkey", &bk_args, &empty_ops, 0);
}
pub use super::compaudit::{compaudit, CompauditError};
#[derive(Clone, Debug)]
pub enum CompDef {
Commands(Vec<String>),
Pattern(Vec<String>),
PostPattern(Vec<String>),
Mixed {
commands: Vec<String>,
patterns: Vec<String>,
postpatterns: Vec<String>,
},
KeyBinding { style: String, keys: Vec<String> },
WidgetKey {
widget: String,
style: String,
key: String,
},
}
#[derive(Clone, Debug)]
pub struct CompFile {
pub path: PathBuf,
pub name: String,
pub def: CompFileDef,
pub body: Option<String>,
}
#[derive(Clone, Debug)]
pub enum CompFileDef {
CompDef(CompDef),
Autoload(Vec<String>),
None,
}
#[derive(Debug, Default)]
pub struct CompInitResult {
pub comps: HashMap<String, String>,
pub services: HashMap<String, String>,
pub patcomps: HashMap<String, String>,
pub postpatcomps: HashMap<String, String>,
pub compautos: HashMap<String, String>,
pub files: Vec<CompFile>,
pub keybindings: Vec<(String, String, Vec<String>)>,
pub widgetkeys: Vec<(String, String, String, String)>,
pub scan_time_ms: u64,
pub dirs_scanned: usize,
pub files_scanned: usize,
}
fn parse_first_line(line: &str) -> CompFileDef {
let line = line.trim();
if let Some(rest) = line.strip_prefix("#compdef") {
let rest = rest.trim();
if rest.is_empty() {
return CompFileDef::None;
}
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.is_empty() {
return CompFileDef::None;
}
let leading = parts[0].strip_suffix('n').filter(|f| f.len() == 2);
match leading.unwrap_or(parts[0]) {
"-k" if parts.len() >= 3 => CompFileDef::CompDef(CompDef::KeyBinding {
style: parts[1].to_string(),
keys: parts[2..].iter().map(|s| s.to_string()).collect(),
}),
"-K" if parts.len() >= 4 => CompFileDef::CompDef(CompDef::WidgetKey {
widget: parts[1].to_string(),
style: parts[2].to_string(),
key: parts[3].to_string(),
}),
flag => {
let mut ty = match flag {
"-p" => 1,
"-P" => 2,
_ => 0,
};
let start = usize::from(ty != 0);
let mut commands: Vec<String> = Vec::new();
let mut patterns: Vec<String> = Vec::new();
let mut postpatterns: Vec<String> = Vec::new();
for word in &parts[start..] {
match *word {
"-N" => ty = 0, "-p" => ty = 1, "-P" => ty = 2, _ => match ty {
1 => patterns.push(word.to_string()),
2 => postpatterns.push(word.to_string()),
_ => commands.push(word.to_string()),
},
}
}
match (
commands.is_empty(),
patterns.is_empty(),
postpatterns.is_empty(),
) {
(true, true, true) => CompFileDef::None,
(false, true, true) => CompFileDef::CompDef(CompDef::Commands(commands)),
(true, false, true) => CompFileDef::CompDef(CompDef::Pattern(patterns)),
(true, true, false) => CompFileDef::CompDef(CompDef::PostPattern(postpatterns)),
_ => CompFileDef::CompDef(CompDef::Mixed {
commands,
patterns,
postpatterns,
}),
}
}
}
} else if let Some(rest) = line.strip_prefix("#autoload") {
let opts: Vec<String> = rest.split_whitespace().map(|s| s.to_string()).collect();
CompFileDef::Autoload(opts)
} else {
CompFileDef::None
}
}
fn is_context_entry(s: &str) -> bool {
if !s.starts_with('-') {
return false;
}
let base = s.split('=').next().unwrap_or(s);
if base.len() <= 2 {
return base == "-"; }
base.ends_with('-') || base.contains(',')
}
fn scan_file(path: &Path) -> Option<CompFile> {
let name = path.file_name()?.to_string_lossy().to_string();
if !name.starts_with('_') {
return None;
}
if name.contains(';')
|| name.contains('|')
|| name.contains('&')
|| name.ends_with('~')
|| name.ends_with(".zwc")
{
return None;
}
let body = fs::read_to_string(path).ok()?;
let first_line = body.lines().next().unwrap_or("");
let def = parse_first_line(first_line);
Some(CompFile {
path: path.to_path_buf(),
name,
def,
body: Some(body),
})
}
fn scan_directory(dir: &Path) -> Vec<CompFile> {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut paths: Vec<PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file())
.collect();
paths.sort_by(|a, b| {
let name = |p: &Path| {
p.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_default()
};
crate::ported::sort::zstrcmp(&name(a), &name(b), 0)
});
paths.par_iter().filter_map(|p| scan_file(p)).collect()
}
pub fn apply_keybindings(result: &CompInitResult) {
for (func, style, keys) in &result.keybindings {
for key in keys {
install_comp_keybinding(func, style, key, func);
}
}
for (widget, style, key, func) in &result.widgetkeys {
install_comp_keybinding(widget, style, key, func);
}
}
fn install_comp_keybinding(widget: &str, style: &str, key: &str, func: &str) {
let empty_ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let style_dotted = if style.starts_with('.') {
style.to_string()
} else {
format!(".{}", style)
};
let zle_args = [widget.to_string(), style_dotted, func.to_string()];
let _ = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &zle_args, &empty_ops, 0);
let bk_args = [key.to_string(), widget.to_string()];
let _ = crate::ported::zle::zle_keymap::bin_bindkey("bindkey", &bk_args, &empty_ops, 0);
}
const STANDARD_COMP_KEYBINDINGS: &[(&str, &str, &str, &str)] = &[
(
"_complete_debug",
"complete-word",
"\u{18}?",
"_complete_debug",
), (
"_complete_help",
"complete-word",
"\u{18}h",
"_complete_help",
), ("_complete_tag", "complete-word", "\u{18}t", "_complete_tag"), (
"_correct_filename",
"complete-word",
"\u{18}C",
"_correct_filename",
), ("_correct_word", "complete-word", "\u{18}c", "_correct_word"), ("_read_comp", "complete-word", "\u{18}\u{12}", "_read_comp"), (
"_most_recent_file",
"complete-word",
"\u{18}m",
"_most_recent_file",
), ("_next_tags", "list-choices", "\u{18}n", "_next_tags"), ("_expand_word", "complete-word", "\u{18}e", "_expand_word"), (
"_list_expansions",
"list-choices",
"\u{18}d",
"_expand_word",
), (
"_bash_complete-word",
"complete-word",
"\u{1b}~",
"_bash_completions",
), (
"_bash_list-choices",
"list-choices",
"\u{18}~",
"_bash_completions",
), (
"_history-complete-older",
"complete-word",
"\u{1b}/",
"_history_complete_word",
), (
"_history-complete-newer",
"complete-word",
"\u{1b},",
"_history_complete_word",
), ("_expand_alias", "complete-word", "\u{18}a", "_expand_alias"), ];
pub fn install_standard_comp_keybindings() {
for (widget, style, key, func) in STANDARD_COMP_KEYBINDINGS {
install_comp_keybinding(widget, style, key, func);
}
}
pub fn compinit(fpath: &[PathBuf]) -> CompInitResult {
let start = Instant::now();
if crate::ported::params::getsparam("_comp_dumpfile")
.map(|s| s.is_empty())
.unwrap_or(true)
{
let _ = crate::ported::params::setsparam(
"_comp_dumpfile",
&default_dumpfile_path().to_string_lossy(),
);
}
crate::ported::params::setaparam(
"_comp_options",
COMP_OPTIONS.iter().map(|s| s.to_string()).collect(),
);
let _ = crate::ported::params::setsparam("_comp_setup", COMP_SETUP_EVAL);
init_comp_funcs_arrays();
let scanned: Vec<CompFile> = fpath
.par_iter()
.filter(|dir| dir.as_os_str() != "." && dir.exists())
.flat_map(|dir| scan_directory(dir))
.collect();
let mut seen: HashSet<String> = HashSet::new();
let all_files: Vec<CompFile> = scanned
.into_iter()
.filter(|f| seen.insert(f.name.clone()))
.collect();
let files_scanned = all_files.len();
let dirs_scanned = fpath.len();
let mut result = CompInitResult {
scan_time_ms: start.elapsed().as_millis() as u64,
dirs_scanned,
files_scanned,
..Default::default()
};
for file in &all_files {
match &file.def {
CompFileDef::CompDef(compdef) => {
match compdef {
CompDef::Commands(cmds) => {
for cmd in cmds {
if let Some(eq_pos) = cmd.find('=') {
let cmd_name = &cmd[..eq_pos];
let service = &cmd[eq_pos + 1..];
if !result.comps.contains_key(cmd_name) {
result.comps.insert(cmd_name.to_string(), file.name.clone());
result
.services
.insert(cmd_name.to_string(), service.to_string());
}
} else if !result.comps.contains_key(cmd) {
result.comps.insert(cmd.clone(), file.name.clone());
}
}
}
CompDef::Pattern(pats) => {
for pat in pats {
result.patcomps.insert(pat.clone(), file.name.clone());
}
}
CompDef::PostPattern(pats) => {
for pat in pats {
result.postpatcomps.insert(pat.clone(), file.name.clone());
}
}
CompDef::Mixed {
commands,
patterns,
postpatterns,
} => {
for cmd in commands {
if let Some(eq_pos) = cmd.find('=') {
let cmd_name = &cmd[..eq_pos];
let service = &cmd[eq_pos + 1..];
if !result.comps.contains_key(cmd_name) {
result.comps.insert(cmd_name.to_string(), file.name.clone());
result
.services
.insert(cmd_name.to_string(), service.to_string());
}
} else if !result.comps.contains_key(cmd) {
result.comps.insert(cmd.clone(), file.name.clone());
}
}
for pat in patterns {
result.patcomps.insert(pat.clone(), file.name.clone());
}
for pat in postpatterns {
result.postpatcomps.insert(pat.clone(), file.name.clone());
}
}
CompDef::KeyBinding { style, keys } => {
result
.keybindings
.push((file.name.clone(), style.clone(), keys.clone()));
}
CompDef::WidgetKey { widget, style, key } => {
result.widgetkeys.push((
widget.clone(),
style.clone(),
key.clone(),
file.name.clone(),
));
}
}
}
CompFileDef::Autoload(opts) => {
let opts_str = opts.join(" ");
result.compautos.insert(file.name.clone(), opts_str);
}
CompFileDef::None => {}
}
}
result.files = all_files;
with_state(|s| {
for (k, v) in &result.comps {
s.comps.insert(k.clone(), v.clone());
}
for (k, v) in &result.services {
s.services.insert(k.clone(), v.clone());
}
for (k, v) in &result.patcomps {
s.patcomps.insert(k.clone(), v.clone());
}
for (k, v) in &result.postpatcomps {
s.postpatcomps.insert(k.clone(), v.clone());
}
for (k, v) in &result.compautos {
s.compautos.insert(k.clone(), v.clone());
}
publish_compdef_state_mut(s);
});
result
}
pub use super::compdump::{check_dump, compdump};
pub fn build_cache_from_fpath(
fpath: &[PathBuf],
cache: &mut crate::compsys::cache::CompsysCache,
) -> std::io::Result<CompInitResult> {
use std::time::Instant;
let t0 = Instant::now();
let result = compinit(fpath);
let scan_time = t0.elapsed();
let t1 = Instant::now();
let comps: Vec<(String, String)> = result
.comps
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
cache
.set_comps_bulk(&comps)
.map_err(|e| std::io::Error::other(e.to_string()))?;
let services: Vec<(String, String)> = result
.services
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
cache
.set_services_bulk(&services)
.map_err(|e| std::io::Error::other(e.to_string()))?;
for (pattern, function) in &result.patcomps {
cache
.set_patcomp(pattern, function)
.map_err(|e| std::io::Error::other(e.to_string()))?;
}
for (pattern, function) in &result.postpatcomps {
cache
.set_postpatcomp(pattern, function)
.map_err(|e| std::io::Error::other(e.to_string()))?;
}
let comps_time = t1.elapsed();
let t2 = Instant::now();
let autoloads: Vec<(String, String, String)> = result
.files
.iter()
.filter(|f| matches!(f.def, CompFileDef::CompDef(_) | CompFileDef::Autoload(_)))
.filter_map(|f| {
let path_str = f.path.to_string_lossy().to_string();
let body = f.body.as_ref()?.clone();
Some((f.name.clone(), path_str, body))
})
.collect();
cache
.add_autoloads_with_bodies_bulk(&autoloads)
.map_err(|e| std::io::Error::other(e.to_string()))?;
let autoloads_time = t2.elapsed();
Ok(result)
}
#[allow(clippy::field_reassign_with_default)] pub fn load_from_cache(
cache: &crate::compsys::cache::CompsysCache,
) -> std::io::Result<CompInitResult> {
use std::time::Instant;
let start = Instant::now();
let mut result = CompInitResult::default();
result.comps = cache
.get_all_comps()
.map_err(|e| std::io::Error::other(e.to_string()))?;
for (pat, func) in cache
.patcomps_kv()
.map_err(|e| std::io::Error::other(e.to_string()))?
{
result.patcomps.insert(pat, func);
}
for (pat, func) in cache
.postpatcomps_kv()
.map_err(|e| std::io::Error::other(e.to_string()))?
{
result.postpatcomps.insert(pat, func);
}
result.scan_time_ms = start.elapsed().as_millis() as u64;
result.files_scanned = result.comps.len();
Ok(result)
}
pub fn cache_entry_count(cache: &crate::compsys::cache::CompsysCache) -> usize {
cache.comp_count().unwrap_or(0) as usize
}
pub fn compinit_lazy(cache: &crate::compsys::cache::CompsysCache) -> (bool, usize) {
let count = cache.comp_count().unwrap_or(0) as usize;
(count > 0, count)
}
pub const CACHE_COMPLETE_KEY: &str = "comps_rows_at_build_end";
pub fn stamp_cache_complete(cache: &crate::compsys::cache::CompsysCache) -> bool {
match cache.comp_count() {
Ok(n) => cache
.set_metadata(CACHE_COMPLETE_KEY, &n.to_string())
.is_ok(),
Err(_) => false,
}
}
pub fn cache_is_valid(cache: &crate::compsys::cache::CompsysCache) -> bool {
let rows = cache.comp_count().unwrap_or(0);
if rows <= 0 {
return false;
}
match cache.get_metadata(CACHE_COMPLETE_KEY) {
Ok(Some(stamp)) => stamp.parse::<i64>().map(|n| n == rows).unwrap_or(false),
_ => false,
}
}
pub fn get_system_fpath() -> Vec<PathBuf> {
if let Ok(fpath_str) = std::env::var("FPATH") {
if !fpath_str.is_empty() {
return fpath_str.split(':').map(PathBuf::from).collect();
}
}
let mut paths = Vec::new();
for base in &["/opt/homebrew", "/usr/local"] {
paths.push(PathBuf::from(format!("{}/share/zsh/site-functions", base)));
paths.push(PathBuf::from(format!("{}/share/zsh/functions", base)));
}
for version in &["5.9", "5.8", "5.7"] {
paths.push(PathBuf::from(format!(
"/usr/share/zsh/{}/functions",
version
)));
}
paths.push(PathBuf::from("/usr/share/zsh/functions"));
paths.push(PathBuf::from("/usr/share/zsh/site-functions"));
if let Ok(home) = std::env::var("HOME") {
paths.push(PathBuf::from(format!("{}/.zinit/completions", home)));
paths.push(PathBuf::from(format!("{}/.zplugin/completions", home)));
paths.push(PathBuf::from(format!(
"{}/.local/share/zsh/site-functions",
home
)));
}
paths.into_iter().filter(|p| p.exists()).collect()
}
#[derive(Clone, Debug, Default)]
pub struct CompInitOpts {
pub dump_file: Option<PathBuf>,
pub no_dump: bool,
pub no_check: bool,
pub ignore_insecure: bool,
pub use_insecure: bool,
}
impl CompInitOpts {
pub fn parse(args: &[String]) -> Self {
let mut opts = Self::default();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-d" if i + 1 < args.len() && !args[i + 1].starts_with('-') => {
opts.dump_file = Some(PathBuf::from(&args[i + 1]));
i += 1;
}
"-D" => opts.no_dump = true,
"-C" => opts.no_check = true,
"-i" => opts.ignore_insecure = true,
"-u" => opts.use_insecure = true,
_ => {}
}
i += 1;
}
opts
}
}
#[derive(Default)]
pub struct CompdefState {
pub comps: HashMap<String, String>,
pub services: HashMap<String, String>,
pub patcomps: HashMap<String, String>,
pub postpatcomps: HashMap<String, String>,
pub compautos: HashMap<String, String>,
removed: CompdefRemovals,
}
#[derive(Default)]
struct CompdefRemovals {
comps: Vec<String>,
services: Vec<String>,
patcomps: Vec<String>,
postpatcomps: Vec<String>,
}
static COMPDEF_STATE: Mutex<Option<CompdefState>> = Mutex::new(None);
static PUBLISH_DEPTH: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
fn with_state<F, R>(f: F) -> R
where
F: FnOnce(&mut CompdefState) -> R,
{
let mut guard = COMPDEF_STATE.lock().unwrap();
if guard.is_none() {
*guard = Some(CompdefState::default());
}
f(guard.as_mut().unwrap())
}
pub fn compdef_batch<R>(f: impl FnOnce() -> R) -> R {
use std::sync::atomic::Ordering;
struct Depth;
impl Drop for Depth {
fn drop(&mut self) {
PUBLISH_DEPTH.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
PUBLISH_DEPTH.fetch_add(1, Ordering::Relaxed);
let out = {
let _depth = Depth;
f()
};
with_state(publish_compdef_state_mut);
out
}
fn merge_hparam(name: &str, set: &HashMap<String, String>, remove: &[String]) {
if set.is_empty() && remove.is_empty() {
return;
}
let mut merged: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
if let Some(existing) = crate::ported::subst::assoc_get(name) {
for (k, v) in existing {
merged.insert(k, v);
}
}
for k in remove {
merged.remove(k);
}
for (k, v) in set {
merged.insert(k.clone(), v.clone());
}
let mut out = Vec::with_capacity(merged.len() * 2);
for (k, v) in merged {
out.push(k);
out.push(v);
}
crate::ported::params::sethparam(name, out);
}
fn publish_compdef_state_mut(s: &mut CompdefState) {
use std::sync::atomic::Ordering;
if PUBLISH_DEPTH.load(Ordering::Relaxed) > 0 {
return;
}
merge_hparam("_comps", &s.comps, &s.removed.comps);
merge_hparam("_services", &s.services, &s.removed.services);
merge_hparam("_patcomps", &s.patcomps, &s.removed.patcomps);
merge_hparam("_postpatcomps", &s.postpatcomps, &s.removed.postpatcomps);
merge_hparam("_compautos", &s.compautos, &[]);
s.removed = CompdefRemovals::default();
}
fn hparam_has_key(name: &str, key: &str) -> bool {
crate::ported::subst::assoc_get(name)
.map(|m| m.contains_key(key))
.unwrap_or(false)
}
#[derive(Default, Debug)]
struct CompdefFlags {
autol: bool,
new: bool,
delete: bool,
eval: bool,
spec_type: SpecType,
}
#[derive(Default, Debug, PartialEq, Clone, Copy)]
enum SpecType {
#[default]
Normal,
Pattern,
PostPattern,
Key,
WidgetKey,
}
fn parse_compdef_flags(args: &[String]) -> Result<(CompdefFlags, usize), String> {
let mut flags = CompdefFlags::default();
let mut idx = 0usize;
while idx < args.len() {
let a = &args[idx];
if !a.starts_with('-') || a == "-" || a == "--" {
break;
}
for c in a.chars().skip(1) {
match c {
'a' => flags.autol = true,
'n' => flags.new = true,
'd' => flags.delete = true,
'e' => flags.eval = true,
'p' => flags.spec_type = SpecType::Pattern,
'P' => flags.spec_type = SpecType::PostPattern,
'k' => flags.spec_type = SpecType::Key,
'K' => flags.spec_type = SpecType::WidgetKey,
_ => return Err(format!("compdef: unknown option: -{}", c)),
}
}
idx += 1;
}
Ok((flags, idx))
}
pub fn compdef(args: &[String]) -> i32 {
if args.is_empty() {
eprintln!("compdef: I need arguments");
return 1;
}
let (flags, mut idx) = match parse_compdef_flags(args) {
Ok(p) => p,
Err(e) => {
eprintln!("{}", e);
return 1;
}
};
if idx >= args.len() {
eprintln!("compdef: I need arguments");
return 1;
}
if flags.delete {
let names = &args[idx..];
with_state(|s| match flags.spec_type {
SpecType::Pattern => {
for n in names {
s.patcomps.remove(n);
s.removed.patcomps.push(n.clone());
}
}
SpecType::PostPattern => {
for n in names {
s.postpatcomps.remove(n);
s.removed.postpatcomps.push(n.clone());
}
}
SpecType::Key | SpecType::WidgetKey => {
eprintln!("compdef: cannot restore key bindings");
}
SpecType::Normal => {
for n in names {
s.comps.remove(n);
s.services.remove(n);
s.removed.comps.push(n.clone());
s.removed.services.push(n.clone());
}
}
});
with_state(publish_compdef_state_mut);
return 0;
}
if !flags.eval && args[idx].contains('=') {
let mut ret: i32 = 0;
while idx < args.len() {
let entry = args[idx].clone();
idx += 1;
if !entry.contains('=') {
eprintln!("compdef: invalid argument: {}", entry);
ret = 1;
continue;
}
let mut sp = entry.splitn(2, '=');
let cmd = sp.next().unwrap_or("").to_string();
let svc_in = sp.next().unwrap_or("").to_string();
let comps_param = crate::ported::subst::assoc_get("_comps").unwrap_or_default();
let resolved_svc = svc_in.clone();
let func = comps_param
.get(&resolved_svc)
.filter(|f| !f.is_empty())
.cloned()
.or_else(|| {
with_state(|s| s.comps.get(&resolved_svc).cloned()).filter(|f| !f.is_empty())
})
.or_else(|| {
let pat = crate::ported::subst::assoc_get("_patcomps").unwrap_or_default();
let postpat =
crate::ported::subst::assoc_get("_postpatcomps").unwrap_or_default();
pat.iter()
.find(|(k, _)| pattern_matches(k, &svc_in))
.map(|(_, v)| v.clone())
.or_else(|| {
postpat
.iter()
.find(|(k, _)| pattern_matches(k, &svc_in))
.map(|(_, v)| v.clone())
})
.or_else(|| {
with_state(|s| {
s.patcomps
.iter()
.find(|(k, _)| pattern_matches(k, &svc_in))
.map(|(_, v)| v.clone())
.or_else(|| {
s.postpatcomps
.iter()
.find(|(k, _)| pattern_matches(k, &svc_in))
.map(|(_, v)| v.clone())
})
})
})
})
.unwrap_or_default();
if func.is_empty() {
eprintln!("compdef: unknown command or service: {}", svc_in);
ret = 1;
continue;
}
let services_param = crate::ported::subst::assoc_get("_services").unwrap_or_default();
let svc_for_state = services_param
.get(&svc_in)
.filter(|v| !v.is_empty())
.cloned()
.or_else(|| with_state(|s| s.services.get(&svc_in).cloned()))
.unwrap_or(svc_in.clone());
with_state(|s| {
s.comps.insert(cmd.clone(), func.clone());
s.services.insert(cmd, svc_for_state);
});
}
with_state(publish_compdef_state_mut);
return ret;
}
let func = args[idx].clone();
idx += 1;
if flags.autol && func.starts_with('_') {
let _ = crate::ported::exec::dispatch_function_call(
"autoload",
&["-rUz".to_string(), func.clone()],
);
with_state(|s| {
s.compautos.insert(func.clone(), "-rUz".to_string());
});
}
match flags.spec_type {
SpecType::WidgetKey => {
let mut i = idx;
while i + 2 < args.len() {
let mut wname = args[i].clone();
let mut comp_widget = args[i + 1].clone();
let key = args[i + 2].clone();
if !wname.starts_with('_') {
wname = format!("_{}", wname);
}
if !comp_widget.starts_with('.') {
comp_widget = format!(".{}", comp_widget);
}
install_comp_keybinding(&wname, &comp_widget, &key, &func);
i += 3;
}
}
SpecType::Key => {
if idx >= args.len() {
eprintln!("compdef: missing keys");
return 1;
}
let mut style = args[idx].clone();
idx += 1;
if !style.starts_with('.') {
style = format!(".{}", style);
}
for key in &args[idx..] {
install_comp_keybinding(&func, &style, key, &func);
}
}
_ => {
let mut effective_type = flags.spec_type;
while idx < args.len() {
let arg = args[idx].clone();
idx += 1;
match arg.as_str() {
"-N" => {
effective_type = SpecType::Normal;
continue;
}
"-p" => {
effective_type = SpecType::Pattern;
continue;
}
"-P" => {
effective_type = SpecType::PostPattern;
continue;
}
_ => {}
}
with_state(|s| match effective_type {
SpecType::Pattern => {
if let Some(eq) = arg.find('=') {
let key = arg[..eq].to_string();
let val = arg[eq + 1..].to_string();
s.patcomps.insert(key, format!("={}={}", val, func));
} else {
s.patcomps.insert(arg.clone(), func.clone());
}
}
SpecType::PostPattern => {
if let Some(eq) = arg.find('=') {
let key = arg[..eq].to_string();
let val = arg[eq + 1..].to_string();
s.postpatcomps.insert(key, format!("={}={}", val, func));
} else {
s.postpatcomps.insert(arg.clone(), func.clone());
}
}
_ => {
let (cmd, svc) = if let Some(eq) = arg.find('=') {
(arg[..eq].to_string(), Some(arg[eq + 1..].to_string()))
} else {
(arg.clone(), None)
};
if flags.new
&& (s.comps.contains_key(&cmd) || hparam_has_key("_comps", &cmd))
{
return;
}
s.comps.insert(cmd.clone(), func.clone());
if let Some(svc) = svc {
s.services.insert(cmd, svc);
}
}
});
}
}
}
with_state(publish_compdef_state_mut);
0
}
fn pattern_matches(pat: &str, s: &str) -> bool {
match crate::ported::pattern::patcompile(
&{
let mut __pat_tok = (pat).to_string();
crate::ported::glob::tokenize(&mut __pat_tok);
__pat_tok
},
0,
None,
) {
Some(prog) => crate::ported::pattern::pattry(&prog, s),
None => pat == s,
}
}
#[cfg(test)]
pub fn reset_compdef_state() {
*COMPDEF_STATE.lock().unwrap() = Some(CompdefState::default());
for name in [
"_comps",
"_services",
"_patcomps",
"_postpatcomps",
"_compautos",
] {
crate::ported::params::sethparam(name, Vec::new());
}
}
pub fn snapshot_compdef_state() -> CompdefState {
with_state(|s| CompdefState {
comps: s.comps.clone(),
services: s.services.clone(),
patcomps: s.patcomps.clone(),
postpatcomps: s.postpatcomps.clone(),
compautos: s.compautos.clone(),
removed: CompdefRemovals::default(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_compdef_commands() {
let def = parse_first_line("#compdef git svn hg");
match def {
CompFileDef::CompDef(CompDef::Commands(cmds)) => {
assert_eq!(cmds, vec!["git", "svn", "hg"]);
}
_ => panic!("Expected Commands"),
}
}
#[test]
fn test_parse_compdef_pattern() {
let def = parse_first_line("#compdef -p 'c*'");
match def {
CompFileDef::CompDef(CompDef::Pattern(pats)) => {
assert_eq!(pats, vec!["'c*'".to_string()]);
}
_ => panic!("Expected Pattern"),
}
}
#[test]
fn test_parse_compdef_postpattern_routing() {
match parse_first_line("#compdef -P 'pip[0-9.]#'") {
CompFileDef::CompDef(CompDef::PostPattern(pats)) => {
assert_eq!(pats, vec!["'pip[0-9.]#'".to_string()]);
}
other => panic!("Expected PostPattern, got {:?}", other),
}
}
#[test]
fn test_parse_compdef_trailing_flags_switch_table() {
let line = "#compdef gcc g++ -value-,CFLAGS,-default- -P gcc-* -P g++-* -p early*";
match parse_first_line(line) {
CompFileDef::CompDef(CompDef::Mixed {
commands,
patterns,
postpatterns,
}) => {
assert_eq!(commands, vec!["gcc", "g++", "-value-,CFLAGS,-default-"]);
assert_eq!(patterns, vec!["early*"]);
assert_eq!(postpatterns, vec!["gcc-*", "g++-*"]);
}
other => panic!("Expected Mixed, got {:?}", other),
}
match parse_first_line("#compdef -p pat* -N cmd") {
CompFileDef::CompDef(CompDef::Mixed {
commands,
patterns,
postpatterns,
}) => {
assert_eq!(commands, vec!["cmd"]);
assert_eq!(patterns, vec!["pat*"]);
assert!(postpatterns.is_empty());
}
other => panic!("Expected Mixed, got {:?}", other),
}
match parse_first_line(r#"#compdef squishy "python -m squishy""#) {
CompFileDef::CompDef(CompDef::Commands(cmds)) => {
assert_eq!(cmds, vec!["squishy", "\"python", "-m", "squishy\""]);
}
other => panic!("Expected Commands, got {:?}", other),
}
}
#[test]
fn test_parse_autoload() {
let def = parse_first_line("#autoload -U -z");
match def {
CompFileDef::Autoload(opts) => {
assert_eq!(opts, vec!["-U", "-z"]);
}
_ => panic!("Expected Autoload"),
}
}
#[test]
fn test_parse_compdef_key() {
let def = parse_first_line("#compdef -k complete-word ^X^C");
match def {
CompFileDef::CompDef(CompDef::KeyBinding { style, keys }) => {
assert_eq!(style, "complete-word");
assert_eq!(keys, vec!["^X^C"]);
}
_ => panic!("Expected KeyBinding"),
}
}
#[test]
fn test_parse_compdef_redirect_context() {
let def = parse_first_line("#compdef bzip2 bunzip2 bzcat=bunzip2 bzip2recover -redirect-,<,bunzip2=bunzip2 -redirect-,>,bzip2=bunzip2 -redirect-,<,bzip2=bzip2");
match def {
CompFileDef::CompDef(CompDef::Commands(cmds)) => {
assert!(cmds.contains(&"bzip2".to_string()), "missing bzip2");
assert!(cmds.contains(&"bunzip2".to_string()), "missing bunzip2");
assert!(
cmds.contains(&"bzcat=bunzip2".to_string()),
"missing bzcat=bunzip2"
);
assert!(
cmds.contains(&"bzip2recover".to_string()),
"missing bzip2recover"
);
assert!(
cmds.contains(&"-redirect-,<,bunzip2=bunzip2".to_string()),
"missing redirect bunzip2"
);
assert!(
cmds.contains(&"-redirect-,>,bzip2=bunzip2".to_string()),
"missing redirect >,bzip2"
);
assert!(
cmds.contains(&"-redirect-,<,bzip2=bzip2".to_string()),
"missing redirect <,bzip2"
);
assert_eq!(cmds.len(), 7, "cmds: {:?}", cmds);
}
other => panic!("Expected Commands, got {:?}", other),
}
}
#[test]
fn test_parse_compdef_context_entries() {
let def = parse_first_line("#compdef -default-");
match def {
CompFileDef::CompDef(CompDef::Commands(cmds)) => {
assert_eq!(cmds, vec!["-default-"]);
}
other => panic!("Expected Commands, got {:?}", other),
}
let def = parse_first_line("#compdef - nohup eval time");
match def {
CompFileDef::CompDef(CompDef::Commands(cmds)) => {
assert!(cmds.contains(&"-".to_string()));
assert!(cmds.contains(&"nohup".to_string()));
assert!(cmds.contains(&"eval".to_string()));
assert!(cmds.contains(&"time".to_string()));
}
other => panic!("Expected Commands, got {:?}", other),
}
let def = parse_first_line("#compdef -value- -array-value- -value-,-default-,-default-");
match def {
CompFileDef::CompDef(CompDef::Commands(cmds)) => {
assert!(cmds.contains(&"-value-".to_string()));
assert!(cmds.contains(&"-array-value-".to_string()));
assert!(cmds.contains(&"-value-,-default-,-default-".to_string()));
}
other => panic!("Expected Commands, got {:?}", other),
}
}
#[test]
fn test_is_context_entry() {
assert!(is_context_entry("-default-"));
assert!(is_context_entry("-redirect-"));
assert!(is_context_entry("-value-,DISPLAY,-default-"));
assert!(is_context_entry("-redirect-,<,bunzip2=bunzip2"));
assert!(is_context_entry("-redirect-,>,bzip2"));
assert!(!is_context_entry("-p")); assert!(!is_context_entry("-P")); assert!(!is_context_entry("git")); }
fn run(args: &[&str]) -> i32 {
let owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
compdef(&owned)
}
#[test]
fn compdef_empty_args_errors() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(compdef(&[]), 1);
}
#[test]
fn compdef_normal_registration() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["_git", "git", "git-commit", "git-push"]), 0);
let state = snapshot_compdef_state();
assert_eq!(state.comps.get("git"), Some(&"_git".to_string()));
assert_eq!(state.comps.get("git-commit"), Some(&"_git".to_string()));
assert_eq!(state.comps.get("git-push"), Some(&"_git".to_string()));
}
#[test]
fn compdef_normal_with_service() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["_git", "hub=git"]), 0);
let state = snapshot_compdef_state();
assert_eq!(state.comps.get("hub"), Some(&"_git".to_string()));
assert_eq!(state.services.get("hub"), Some(&"git".to_string()));
}
#[test]
fn compdef_pattern_via_dash_p() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["-p", "_test", "*-test"]), 0);
let state = snapshot_compdef_state();
assert_eq!(state.patcomps.get("*-test"), Some(&"_test".to_string()));
}
#[test]
fn compdef_postpattern_via_dash_p_caps() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["-P", "_last", "_*"]), 0);
let state = snapshot_compdef_state();
assert_eq!(state.postpatcomps.get("_*"), Some(&"_last".to_string()));
}
#[test]
fn compdef_pattern_with_eq_rewrites_to_eq_form() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["-p", "_test", "*=postfix"]), 0);
let state = snapshot_compdef_state();
assert_eq!(state.patcomps.get("*"), Some(&"=postfix=_test".to_string()));
}
#[test]
fn compdef_delete_removes_from_comps() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
run(&["_git", "git"]);
assert!(snapshot_compdef_state().comps.contains_key("git"));
assert_eq!(run(&["-d", "git"]), 0);
assert!(!snapshot_compdef_state().comps.contains_key("git"));
}
#[test]
fn compdef_delete_pattern_removes_from_patcomps() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
run(&["-p", "_test", "*-test"]);
assert!(snapshot_compdef_state().patcomps.contains_key("*-test"));
assert_eq!(run(&["-d", "-p", "*-test"]), 0);
assert!(!snapshot_compdef_state().patcomps.contains_key("*-test"));
}
#[test]
fn compdef_no_clobber_skips_existing() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
run(&["_first", "git"]);
run(&["-n", "_second", "git"]);
assert_eq!(
snapshot_compdef_state().comps.get("git"),
Some(&"_first".to_string())
);
}
#[test]
fn compdef_no_clobber_honours_a_registration_only_the_parameter_holds() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
crate::ported::params::sethparam(
"_comps",
vec!["git".to_string(), "_git_from_dump".to_string()],
);
run(&["-n", "_second", "git"]);
assert_eq!(
crate::ported::subst::assoc_get("_comps")
.and_then(|m| m.get("git").cloned())
.as_deref(),
Some("_git_from_dump")
);
}
#[test]
fn compdef_keeps_registrations_it_did_not_make() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
crate::ported::params::sethparam(
"_comps",
vec![
"man".to_string(),
"_man".to_string(),
"git".to_string(),
"_git".to_string(),
],
);
assert_eq!(run(&["_zstyle", "zstyle"]), 0);
let comps = crate::ported::subst::assoc_get("_comps").expect("_comps must still be a hash");
assert_eq!(comps.get("man").map(String::as_str), Some("_man"));
assert_eq!(comps.get("git").map(String::as_str), Some("_git"));
assert_eq!(comps.get("zstyle").map(String::as_str), Some("_zstyle"));
}
#[test]
fn compdef_delete_removes_a_key_only_the_parameter_holds() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
crate::ported::params::sethparam(
"_comps",
vec![
"man".to_string(),
"_man".to_string(),
"git".to_string(),
"_git".to_string(),
],
);
assert_eq!(run(&["-d", "man"]), 0);
let comps = crate::ported::subst::assoc_get("_comps").expect("_comps must still be a hash");
assert_eq!(comps.get("man"), None);
assert_eq!(comps.get("git").map(String::as_str), Some("_git"));
}
#[test]
fn compdef_batch_defers_publication_but_still_publishes() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
compdef_batch(|| {
run(&["_git", "git"]);
assert!(
crate::ported::subst::assoc_get("_comps")
.map(|m| m.is_empty())
.unwrap_or(true),
"publication must be held until the batch ends"
);
run(&["_man", "man"]);
});
let comps = crate::ported::subst::assoc_get("_comps").expect("_comps must still be a hash");
assert_eq!(comps.get("git").map(String::as_str), Some("_git"));
assert_eq!(comps.get("man").map(String::as_str), Some("_man"));
}
#[test]
fn cache_is_valid_rejects_a_cache_that_is_still_filling() {
let cache = crate::compsys::cache::CompsysCache::memory().expect("in-memory cache");
assert!(!cache_is_valid(&cache), "an empty cache is not valid");
cache.set_comp("git", "_git").unwrap();
assert!(
!cache_is_valid(&cache),
"a cache no build has stamped is not valid, however many rows it has"
);
assert!(stamp_cache_complete(&cache));
assert!(cache_is_valid(&cache));
cache.set_comp("man", "_man").unwrap();
assert!(
!cache_is_valid(&cache),
"a row written after the stamp means the build was not the last writer"
);
}
#[test]
fn compdef_inline_type_switch_dash_p() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
run(&["_x", "cmd1", "-p", "pat*", "-N", "cmd2"]);
let s = snapshot_compdef_state();
assert_eq!(s.comps.get("cmd1"), Some(&"_x".to_string()));
assert_eq!(s.patcomps.get("pat*"), Some(&"_x".to_string()));
assert_eq!(s.comps.get("cmd2"), Some(&"_x".to_string()));
}
#[test]
fn compdef_combined_flags_an() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["-an", "_git", "git"]), 0);
let s = snapshot_compdef_state();
assert_eq!(s.comps.get("git"), Some(&"_git".to_string()));
assert_eq!(s.compautos.get("_git"), Some(&"-rUz".to_string()));
}
#[test]
fn compdef_service_alias_mode_resolves_existing_func() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
run(&["_git", "git"]); assert_eq!(run(&["hub=git"]), 0);
let s = snapshot_compdef_state();
assert_eq!(s.comps.get("hub"), Some(&"_git".to_string()));
assert_eq!(s.services.get("hub"), Some(&"git".to_string()));
}
#[test]
fn compdef_service_alias_unknown_returns_one() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["xyz=never-registered"]), 1);
}
#[test]
fn compdef_unknown_flag_errors() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
assert_eq!(run(&["-z", "_x", "cmd"]), 1);
}
#[test]
fn compdef_publishes_state_to_shell_arrays() {
let _g = crate::test_util::global_state_lock();
reset_compdef_state();
run(&["_git", "git"]);
let map = crate::ported::params::paramtab_hashed_storage()
.lock()
.unwrap()
.get("_comps")
.cloned()
.expect("_comps must be a hashed (associative) param");
assert_eq!(map.get("git").map(String::as_str), Some("_git"));
}
#[test]
fn scan_keeps_first_fpath_claim_on_a_command() {
let _g = crate::test_util::global_state_lock();
let base = std::env::temp_dir().join("zshrs_compinit_firstwins_test");
let early = base.join("early");
let late = base.join("late");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&early).unwrap();
fs::create_dir_all(&late).unwrap();
fs::write(early.join("_zzcmd"), "#compdef zzcmd zzother\n").unwrap();
fs::write(late.join("_zzgame"), "#compdef zzgame zzcmd\n").unwrap();
let result = compinit(&[early.clone(), late.clone()]);
assert_eq!(
result.comps.get("zzcmd").map(String::as_str),
Some("_zzcmd"),
"earlier fpath dir must keep the command"
);
assert_eq!(
result.comps.get("zzgame").map(String::as_str),
Some("_zzgame")
);
let reversed = compinit(&[late.clone(), early.clone()]);
assert_eq!(
reversed.comps.get("zzcmd").map(String::as_str),
Some("_zzgame")
);
let _ = fs::remove_dir_all(&base);
}
#[test]
fn scan_registers_autoload_stubs_for_every_completer() {
let _g = crate::test_util::global_state_lock();
let dir = std::env::temp_dir().join("zshrs_compinit_stubs_test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("_zzt"), "#compdef zzt\n").unwrap();
fs::write(dir.join("_zzt-helper"), "#compdef zzt-helper\n").unwrap();
fs::write(dir.join("_zzt_util"), "#autoload\n").unwrap();
fs::write(dir.join("_zzt_readme"), "just text\n").unwrap();
for n in ["_zzt", "_zzt-helper", "_zzt_util", "_zzt_readme"] {
if let Ok(mut t) = crate::ported::hashtable::shfunctab_lock().write() {
t.remove(n);
}
}
if let Ok(mut t) = crate::ported::hashtable::shfunctab_lock().write() {
let mut defined = crate::ported::hashtable::shfunc_autoload("_zzt");
defined.node.flags = 0;
defined.body = Some("true".to_string());
t.add(defined);
}
let result = compinit(&[dir.clone()]);
let names = autoload_stub_names(&result);
assert!(names.contains(&"_zzt-helper"), "got {names:?}");
assert!(names.contains(&"_zzt_util"), "got {names:?}");
assert!(!names.contains(&"_zzt_readme"), "got {names:?}");
assert_eq!(
register_autoload_stubs(&names),
2,
"the already-defined _zzt must not be re-stubbed"
);
let tab = crate::ported::hashtable::shfunctab_lock();
let tab = tab.read().unwrap();
for n in ["_zzt-helper", "_zzt_util"] {
let shf = tab.get(n).unwrap_or_else(|| panic!("{n} has no stub"));
let flags = shf.node.flags as u32;
assert!(flags & crate::ported::zsh_h::PM_UNDEFINED != 0, "{n}");
assert!(flags & crate::ported::zsh_h::PM_UNALIASED != 0, "{n}");
}
assert_eq!(
tab.get("_zzt").and_then(|f| f.body.clone()),
Some("true".to_string()),
"an already-defined function must keep its body"
);
assert!(tab.get("_zzt_readme").is_none());
drop(tab);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn dump_autoload_names_reads_both_compdump_line_shapes() {
let dir = std::env::temp_dir().join("zshrs_compinit_dumpnames_test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let dump = dir.join("zcompdump");
fs::write(
&dump,
concat!(
"#files: 3\tversion: 5.9.2\n",
"\n",
"_comps=(\n",
"'autoload' '_autoload'\n",
"'zzt' '_zzt'\n",
")\n",
"\n",
"zle -C _complete_help complete-word _complete_help\n",
"bindkey '^Xh' _complete_help\n",
"\n",
"autoload -Uz _zzt _zzt_two \\\n",
" __zzt_headerless _zzt_gone\n",
"autoload -Uz +X _call_program\n",
"typeset -gUa _comp_assocs\n",
),
)
.unwrap();
let names = dump_autoload_names(&dump);
assert_eq!(
names,
vec![
"_zzt",
"_zzt_two",
"__zzt_headerless",
"_zzt_gone",
"_call_program",
],
"continuation lines, `+X`/`-Uz` option words and the quoted \
`_comps` key must all be handled"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn dump_assoc_tables_reads_all_five_compdump_tables() {
let dir = std::env::temp_dir().join("zshrs_compinit_dumptables_test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let dump = dir.join("zcompdump");
fs::write(
&dump,
concat!(
"#files: 3\tversion: 5.9.2\n",
"\n",
"_comps=(\n",
"'zpwr' '_zpwr'\n",
"''\\''brew' '_brew_services'\n",
"'services'\\''' '_brew_services'\n",
")\n",
"\n",
"_services=(\n",
"'ftp' 'ftp'\n",
")\n",
"\n",
"_patcomps=(\n",
"'*/(init|rc[0-9S]#).d/*' '_init_d'\n",
")\n",
"\n",
"_postpatcomps=(\n",
"'_*' '_compadd'\n",
"'gcc-*' '_gcc'\n",
")\n",
"\n",
"_compautos=(\n",
"'_call_program' '+X'\n",
")\n",
"\n",
"zle -C _complete_help complete-word _complete_help\n",
"autoload -Uz _zzt\n",
"typeset -gUa _comp_assocs\n",
"_comp_assocs=( '' )\n",
),
)
.unwrap();
let t = dump_assoc_tables(&dump).expect("dump is readable");
assert_eq!(t.comps.get("zpwr").map(String::as_str), Some("_zpwr"));
assert_eq!(
t.comps.get("'brew").map(String::as_str),
Some("_brew_services"),
"`''\\''brew'` is three concatenated (qq) segments = `'brew`"
);
assert_eq!(
t.comps.get("services'").map(String::as_str),
Some("_brew_services")
);
assert_eq!(t.comps.len(), 3, "no stray keys from the trailing lines");
assert_eq!(t.services.get("ftp").map(String::as_str), Some("ftp"));
assert_eq!(
t.patcomps.get("*/(init|rc[0-9S]#).d/*").map(String::as_str),
Some("_init_d")
);
assert_eq!(
t.postpatcomps.keys().map(String::as_str).collect::<Vec<_>>(),
vec!["_*", "gcc-*"]
);
assert_eq!(t.compautos.get("_call_program").map(String::as_str), Some("+X"));
let _ = fs::remove_dir_all(&dir);
}
}