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 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
}
pub fn maybe_rebind_tab_for_expand() {
let curcontext = crate::ported::params::getsparam("curcontext").unwrap_or_default();
let completers = crate::ported::modules::zutil::lookupstyle(
&format!(":completion:{}:", curcontext),
"completer",
);
let has_expand = completers
.iter()
.any(|c| c == "_expand" || c.starts_with("_expand:"));
if !has_expand {
return;
}
let _ = crate::ported::exec::dispatch_function_call(
"bindkey",
&["^i".to_string(), "complete-word".to_string()],
);
}
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(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_patcomp(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);
}
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 fn cache_is_valid(cache: &crate::compsys::cache::CompsysCache) -> bool {
cache.comp_count().unwrap_or(0) > 0
}
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>,
}
static COMPDEF_STATE: Mutex<Option<CompdefState>> = Mutex::new(None);
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())
}
fn publish_compdef_state_mut(s: &mut CompdefState) {
publish_compdef_state(s);
}
fn publish_compdef_state(s: &CompdefState) {
let mut flatten = |arr: &HashMap<String, String>| -> Vec<String> {
let mut sorted: Vec<(&String, &String)> = arr.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(b.0));
let mut out = Vec::with_capacity(sorted.len() * 2);
for (k, v) in sorted {
out.push(k.clone());
out.push(v.clone());
}
out
};
crate::ported::params::sethparam("_comps", flatten(&s.comps));
crate::ported::params::sethparam("_services", flatten(&s.services));
crate::ported::params::sethparam("_patcomps", flatten(&s.patcomps));
crate::ported::params::sethparam("_postpatcomps", flatten(&s.postpatcomps));
crate::ported::params::sethparam("_compautos", flatten(&s.compautos));
}
#[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);
}
}
SpecType::PostPattern => {
for n in names {
s.postpatcomps.remove(n);
}
}
SpecType::Key | SpecType::WidgetKey => {
eprintln!("compdef: cannot restore key bindings");
}
SpecType::Normal => {
for n in names {
s.comps.remove(n);
s.services.remove(n);
}
}
});
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) {
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());
}
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(),
})
}
#[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_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);
}
}