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());
}
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 mut count = 0usize;
for w in STANDARD_COMPLETE_WIDGETS {
let dot_w = format!(".{}", w);
if crate::ported::exec_hooks::dispatch_function_call(
"zle",
&["-C".to_string(), w.to_string(), dot_w, "_main_complete".to_string()],
)
.is_some()
{
count += 1;
}
}
if crate::ported::exec_hooks::dispatch_function_call(
"zle",
&["-la".to_string(), "menu-select".to_string()],
)
.map(|rc| rc == 0)
.unwrap_or(false)
{
if crate::ported::exec_hooks::dispatch_function_call(
"zle",
&[
"-C".to_string(),
"menu-select".to_string(),
".menu-select".to_string(),
"_main_complete".to_string(),
],
)
.is_some()
{
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_hooks::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(String),
PostPattern(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 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;
}
match parts[0] {
"-p" if parts.len() >= 2 => {
CompFileDef::CompDef(CompDef::Pattern(parts[1].to_string()))
}
"-P" if parts.len() >= 2 => {
let mut all_cmds = Vec::new();
let mut i = 0;
while i < parts.len() {
if parts[i] == "-P" && i + 1 < parts.len() {
all_cmds.push(parts[i + 1].to_string());
i += 2;
} else if !parts[i].starts_with('-') || is_context_entry(parts[i]) {
all_cmds.push(parts[i].to_string());
i += 1;
} else {
i += 1;
}
}
if all_cmds.is_empty() {
CompFileDef::None
} else {
CompFileDef::CompDef(CompDef::Commands(all_cmds))
}
}
"-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(),
}),
_ => {
let cmds: Vec<String> = parts
.iter()
.filter(|s| {
**s == "-" || is_context_entry(s) || !s.starts_with('-')
})
.map(|s| s.to_string())
.collect();
if cmds.is_empty() {
CompFileDef::None
} else {
CompFileDef::CompDef(CompDef::Commands(cmds))
}
}
}
} 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 paths: Vec<PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file())
.collect();
paths.par_iter().filter_map(|p| scan_file(p)).collect()
}
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 seen: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
let all_files: Vec<CompFile> = fpath
.par_iter()
.filter(|dir| dir.as_os_str() != "." && dir.exists())
.flat_map(|dir| scan_directory(dir))
.filter(|f| {
let mut seen = seen.lock().unwrap();
if seen.contains(&f.name) {
false
} else {
seen.insert(f.name.clone());
true
}
})
.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..];
result.comps.insert(cmd_name.to_string(), file.name.clone());
result
.services
.insert(cmd_name.to_string(), service.to_string());
} else {
result.comps.insert(cmd.clone(), file.name.clone());
}
}
}
CompDef::Pattern(pat) => {
result.comps.insert(pat.clone(), file.name.clone());
result.patcomps.insert(pat.clone(), file.name.clone());
}
CompDef::PostPattern(pat) => {
result.postpatcomps.insert(pat.clone(), file.name.clone());
}
CompDef::KeyBinding { .. } | CompDef::WidgetKey { .. } => {
}
}
}
CompFileDef::Autoload(opts) => {
let opts_str = opts.join(" ");
result.compautos.insert(file.name.clone(), opts_str);
}
CompFileDef::None => {}
}
}
result.files = all_files;
install_standard_complete_widgets();
maybe_rebind_tab_for_expand();
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::setaparam("_comps", flatten(&s.comps));
crate::ported::params::setaparam("_services", flatten(&s.services));
crate::ported::params::setaparam("_patcomps", flatten(&s.patcomps));
crate::ported::params::setaparam("_postpatcomps", flatten(&s.postpatcomps));
crate::ported::params::setaparam("_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 resolved_svc = {
with_state(|s| {
s.services
.iter()
.find(|(_, v)| **v == svc_in)
.map(|(k, _)| k.clone())
.unwrap_or(svc_in.clone())
})
};
let func = with_state(|s| {
s.comps
.get(&resolved_svc)
.cloned()
.or_else(|| {
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 svc_for_state = 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_hooks::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);
}
let _ = crate::ported::exec_hooks::dispatch_function_call(
"zle",
&[
"-C".to_string(),
wname.clone(),
comp_widget,
func.clone(),
],
);
let _ = crate::ported::exec_hooks::dispatch_function_call(
"bindkey",
&[key, wname],
);
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);
}
let _ = crate::ported::exec_hooks::dispatch_function_call(
"zle",
&["-C".to_string(), func.clone(), style, func.clone()],
);
for key in &args[idx..] {
let _ = crate::ported::exec_hooks::dispatch_function_call(
"bindkey",
&[key.clone(), func.clone()],
);
}
}
_ => {
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(pat, 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(pat)) => {
assert_eq!(pat, "'c*'");
}
_ => panic!("Expected Pattern"),
}
}
#[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 arr = crate::ported::params::getaparam("_comps").unwrap_or_default();
assert!(arr.windows(2).any(|w| w[0] == "git" && w[1] == "_git"));
}
}