use crate::compsys::ported::_all_labels::_all_labels;
use crate::compsys::ported::_description::_description;
use crate::compsys::ported::_message::_message;
use crate::compsys::ported::_next_label::_next_label;
use crate::compsys::ported::_requested::_requested;
use crate::compsys::ported::_tags::_tags;
use crate::ported::exec::{dispatch_function_call, execute_script};
use crate::ported::glob::matchpat;
use crate::ported::modules::zutil::zstyletab;
use crate::ported::params::{
getaparam, getiparam, getsparam, setaparam, setiparam, setsparam, unsetparam,
};
use crate::ported::zle::compcore::{get_compstate_str, set_compstate_str};
use crate::ported::zle::complete::bin_compadd;
use crate::ported::zle::computil::bin_comparguments;
use crate::ported::zsh_h::{options, MAX_OPS};
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn comparguments(argv: &[&str]) -> i32 {
let v: Vec<String> = argv.iter().map(|s| s.to_string()).collect();
bin_comparguments("comparguments", &v, &make_ops(), 0)
}
fn strip_colon_desc(arr: &[String]) -> Vec<String> {
arr.iter()
.map(|s| s.splitn(2, ':').next().unwrap_or("").to_string())
.collect()
}
fn prefix_suffix() -> String {
format!(
"{}{}",
getsparam("PREFIX").unwrap_or_default(),
getsparam("SUFFIX").unwrap_or_default()
)
}
fn nmatches() -> i64 {
get_compstate_str("nmatches")
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
fn strip_last_field(ctx: &str) -> String {
match ctx.rfind(':') {
Some(i) => ctx[..i].to_string(),
None => ctx.to_string(),
}
}
fn sanitize_cache_name(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
.collect()
}
fn strip_to_wordchars(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
.collect()
}
fn strip_trailing_nonword(s: &str) -> String {
s.trim_end_matches(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '-'))
.to_string()
}
fn desc_transform(s: &str) -> String {
s.replace(':', "-").replace('[', "(").replace(']', ")")
}
fn lopts_add_distributed(lopts: &mut Vec<String>, tmp: &[String], suffix: &str) {
for e in tmp {
let v = format!("{}:{}", e, suffix);
if !lopts.contains(&v) {
lopts.push(v);
}
}
}
fn split_spec_pattern(spec: &str) -> (String, String) {
let chars: Vec<char> = spec.chars().collect();
let mut idx = None;
for (j, &c) in chars.iter().enumerate() {
if c == ':' && j > 0 && chars[j - 1] != '\\' {
idx = Some(j);
break;
}
}
match idx {
Some(k) => {
let pat: String = chars[..k].iter().collect::<String>().replace("\\:", ":");
let descr: String = chars[k..].iter().collect();
(pat, descr)
}
None => (spec.to_string(), String::new()),
}
}
fn strip_leading_argword(s: &str) -> String {
let b: Vec<char> = s.chars().collect();
if b.first() == Some(&' ') {
let mut i = 1;
while i < b.len() && b[i] != ' ' {
i += 1;
}
if i > 1 && i + 1 < b.len() && b[i] == ' ' && b[i + 1] == ' ' {
return b[i + 2..].iter().collect();
}
}
s.to_string()
}
fn split_last_colon(s: &str) -> (String, Option<String>) {
match s.rfind(':') {
Some(i) => (s[..i].to_string(), Some(s[i + 1..].to_string())),
None => (s.to_string(), None),
}
}
fn run_help(cmd: &str, use_locale: bool) -> String {
use std::process::{Command, Stdio};
let curcontext = getsparam("curcontext").unwrap_or_default();
let style_ctx = format!(":completion:{}:options", curcontext);
let styled = crate::ported::modules::zutil::lookupstyle(&style_ctx, "command")
.into_iter()
.next()
.unwrap_or_default();
let cmdline = if !styled.is_empty() {
if let Some(rest) = styled.strip_prefix('-') {
format!("{} {} --help", rest, cmd)
} else {
styled
}
} else {
format!("{} --help", cmd)
};
let mut c = Command::new("sh");
c.arg("-c").arg(format!("{} 2>&1", cmdline));
c.env("COLUMNS", "999"); c.stdin(Stdio::null());
if use_locale {
for (k, _) in std::env::vars() {
if k.starts_with("LC_") && k != "LC_CTYPE" {
c.env_remove(&k);
}
}
let ctype = std::env::var("LC_ALL")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("LC_CTYPE").ok().filter(|s| !s.is_empty()))
.or_else(|| std::env::var("LANG").ok().filter(|s| !s.is_empty()))
.unwrap_or_else(|| "C".to_string());
c.env("LC_CTYPE", ctype);
c.env("LANG", "C");
}
let _guard = crate::fusevm_bridge::ForegroundWaitGuard::enter();
match c.output() {
Ok(o) => String::from_utf8_lossy(&o.stdout).into_owned(),
Err(_) => String::new(),
}
}
fn long_option_cache(full: &[String], long: usize, cmd: &str) -> Vec<String> {
let tmpargv: Vec<String> = full[..long].to_vec();
let mut name = cmd.to_string();
if let Some(rest) = name.strip_prefix("~/") {
if let Ok(home) = std::env::var("HOME") {
name = format!("{}/{}", home, rest);
}
} else if name == "~" {
if let Ok(home) = std::env::var("HOME") {
name = home;
}
}
if !name.starts_with('/') && name.contains('/') {
let pwd = getsparam("PWD")
.or_else(|| std::env::var("PWD").ok())
.unwrap_or_default();
name = format!("{}/{}", pwd, name);
}
let name = sanitize_cache_name(&format!("_args_cache_{}", name));
let already_set = crate::ported::params::paramtab()
.read()
.map(|t| t.contains_key(name.as_str()))
.unwrap_or(false);
if !already_set {
let cache = build_help_cache(full, long, cmd, &tmpargv);
let cache: Vec<String> = cache
.into_iter()
.filter(|e| !e.bytes().all(|b| b == b' '))
.collect();
setaparam(&name, cache);
}
let mut out = tmpargv;
out.extend(getaparam(&name).unwrap_or_default());
out
}
fn has_unclosed_bracket(p: &str) -> bool {
let b = p.as_bytes();
for i in 0..b.len() {
if b[i] == b'[' && !b[i + 1..].contains(&b']') {
return true;
}
}
false
}
fn scrape_help_lopts(help_text: &str) -> Vec<String> {
let mut lopts: Vec<String> = Vec::new(); let mut tmp: Vec<String> = Vec::new();
for line in help_text.lines() {
let mut opt = line.to_string();
if !tmp.is_empty() {
let leading3_ws = {
let c: Vec<char> = opt.chars().take(3).collect();
c.len() == 3 && c.iter().all(|ch| ch.is_whitespace())
};
let has_alpha_after = opt.chars().skip(3).any(|c| c.is_ascii_alphabetic());
if leading3_ws && has_alpha_after {
opt = opt.trim_start_matches(char::is_whitespace).to_string();
lopts_add_distributed(&mut lopts, &tmp, &desc_transform(&opt));
tmp.clear();
continue;
} else {
lopts_add_distributed(&mut lopts, &tmp, "");
tmp.clear();
}
}
loop {
let trimmed = opt.trim_start_matches(|c: char| c == ',' || c.is_whitespace());
if !trimmed.starts_with('-') {
break;
}
let end = trimmed[1..]
.find(|c: char| c == ',' || c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(trimmed.len());
let start = trimmed[..end].to_string();
let rest = trimmed[end..].to_string();
let cleaned = strip_trailing_nonword(&start);
let dup = if has_unclosed_bracket(&cleaned) {
tmp.iter().any(|e| e == &cleaned)
} else {
tmp.iter().any(|e| matchpat(&cleaned, e, true, true))
};
if !dup {
if start.starts_with("--[") {
if let Some(close) = start.rfind(']') {
let inner = &start[3..close];
let after = &start[close + 1..];
tmp.push(format!("--{}{}", inner, after));
tmp.push(format!("--{}", after));
} else {
tmp.push(start.clone());
}
} else {
tmp.push(start.clone());
}
}
opt = rest;
}
opt = strip_leading_argword(&opt);
opt = opt.trim_start_matches(char::is_whitespace).to_string();
if !opt.is_empty() {
lopts_add_distributed(&mut lopts, &tmp, &desc_transform(&opt));
tmp.clear();
}
}
if !tmp.is_empty() {
lopts_add_distributed(&mut lopts, &tmp, "");
}
lopts
}
fn build_help_cache(
full: &[String],
long: usize,
cmd: &str,
tmpargv: &[String],
) -> Vec<String> {
let post: Vec<String> = full[long + 1..].to_vec();
let mut p = 0usize;
let mut iopts: Vec<String> = Vec::new(); let mut sopts: Vec<String> = Vec::new(); let mut use_locale = true; while p < post.len() {
let a = &post[p];
if !(a.len() >= 2 && a.as_bytes()[0] == b'-' && matches!(a.as_bytes()[1], b'l' | b'i' | b's'))
{
break;
}
if a == "-l" {
use_locale = false;
p += 1;
continue;
}
let (raw, cur) = if a.len() > 2 {
(a[2..].to_string(), 1usize)
} else if p + 1 < post.len() {
(post[p + 1].clone(), 2usize)
} else {
(String::new(), 1usize)
};
let vals: Vec<String> = if raw.starts_with('(') {
let inner = raw
.strip_prefix('(')
.map(|s| s.strip_suffix(')').unwrap_or(s))
.unwrap_or(raw.as_str());
inner.split_whitespace().map(|s| s.to_string()).collect()
} else {
getaparam(&raw).unwrap_or_default()
};
if a.as_bytes()[1] == b'i' {
iopts.extend(vals);
} else {
sopts.extend(vals);
}
p += cur;
}
let descr_specs_user: Vec<String> = post[p..].to_vec();
let help_text = run_help(cmd, use_locale);
let mut lopts: Vec<String> = scrape_help_lopts(&help_text);
{
let mut kept: Vec<String> = Vec::new();
let names: Vec<String> = lopts
.iter()
.filter(|e| e.as_str() != "--")
.map(|e| {
let cut = e
.find(|c: char| c == '[' || c == ':' || c == '=')
.unwrap_or(e.len());
e[..cut].to_string()
})
.collect();
for optn in names {
let pat = format!(
"(|\\([^)]#\\))(|\\*){}(|[-+]|=(|-))(|\\[*\\])(|:*)",
optn
);
let covered = tmpargv.iter().any(|s| matchpat(&pat, s, true, true));
if !covered {
let keep_pat = format!("{}(|[\\[:=]*)", optn);
if let Some(found) = lopts.iter().find(|e| matchpat(&keep_pat, e.as_str(), true, true)) {
if !kept.contains(found) {
kept.push(found.clone());
}
}
}
}
lopts = kept;
}
for ig in &iopts {
let pat = format!("{}(|[\\[:=]*)", ig);
lopts.retain(|e| !matchpat(&pat, e, true, true));
}
{
let mut i = 0;
while i + 1 < sopts.len() {
let pat = &sopts[i];
let repl = &sopts[i + 1];
let subs: Vec<String> = lopts
.iter()
.map(|e| subst_first(e, pat, repl))
.collect();
for s in subs {
if !lopts.contains(&s) {
lopts.push(s);
}
}
i += 2;
}
}
let mut descr_specs = descr_specs_user;
descr_specs.push("*=FILE*:file:_files".to_string());
descr_specs.push("*=(DIR|PATH)*:directory:_files -/".to_string());
descr_specs.push("*=*:=: ".to_string());
descr_specs.push("*: : ".to_string());
let mut cache: Vec<String> = Vec::new();
for spec in descr_specs {
let (mut pattern, descr) = split_spec_pattern(&spec);
let dir = if pattern.ends_with("(-)") {
pattern.truncate(pattern.len() - 3);
"-".to_string()
} else {
String::new()
};
let match_pat = format!("{}:*", pattern);
let mut matched: Vec<String> = lopts
.iter()
.filter(|e| matchpat(&match_pat, e.as_str(), true, true))
.cloned()
.collect();
lopts.retain(|e| !matchpat(&match_pat, e, true, true));
if matched.is_empty() {
continue;
}
for e in &mut matched {
if e.ends_with(':') {
e.pop();
}
}
{
let optpat = "[^:]##\\[\\=*";
let tmpo: Vec<String> = matched
.iter()
.filter(|e| matchpat(optpat, e.as_str(), true, true))
.cloned()
.collect();
if !tmpo.is_empty() {
matched.retain(|e| !matchpat(optpat, e.as_str(), true, true));
for opt in tmpo {
let (opt, odescr) = match split_last_colon(&opt) {
(base, Some(d)) => (base, format!("[{}]", d)),
(base, None) => (base, String::new()),
};
let opt2 = if let Some(pos) = opt.rfind("[=") {
format!("{}=-{}{}", strip_to_wordchars(&opt[..pos]), dir, odescr)
} else {
format!("{}={}{}", strip_to_wordchars(&opt), dir, odescr)
};
if descr.starts_with(":=") {
cache.push(format!("{}::{}: ", opt2, arg_name_lower(&opt)));
} else if descr.starts_with("::") {
cache.push(format!("{}{}", opt2, descr)); } else {
cache.push(format!("{}:{}", opt2, descr)); }
}
}
}
{
let eqpat = "[^:]##\\=*";
let tmpo: Vec<String> = matched
.iter()
.filter(|e| matchpat(eqpat, e.as_str(), true, true))
.cloned()
.collect();
if !tmpo.is_empty() {
matched.retain(|e| !matchpat(eqpat, e.as_str(), true, true));
for opt in tmpo {
let (opt, odescr) = match split_last_colon(&opt) {
(base, Some(d)) => (base, format!("[{}]", d)),
(base, None) => (base, String::new()),
};
let base = opt.split('=').next().unwrap_or("");
let opt2 = format!("{}={}{}", strip_to_wordchars(base), dir, odescr);
if descr.starts_with(":=") {
cache.push(format!("{}:{}: ", opt2, arg_name_lower(&opt)));
} else {
cache.push(format!("{}{}", opt2, descr)); }
}
}
}
if !matched.is_empty() {
let mut built: Vec<String> = Vec::new();
for e in &matched {
if let Some(ci) = e.find(':') {
let head = e[..ci].to_string();
let tail = e[ci + 1..].to_string();
built.push(format!("{}[{}]", head, tail.replace(':', "[")));
}
}
for e in &matched {
if !e.contains(':') {
built.push(strip_to_wordchars(e));
}
}
if !descr.is_empty() && descr != ": : " {
for b in built {
cache.push(format!("{}{}", b, descr));
}
} else {
cache.extend(built);
}
}
}
cache
}
fn arg_name_lower(opt: &str) -> String {
let s = opt.strip_suffix(']').unwrap_or(opt);
let after = match s.find('=') {
Some(i) => &s[i + 1..],
None => s,
};
after.to_lowercase()
}
fn subst_first(str_in: &str, pat: &str, repl: &str) -> String {
let chars: Vec<char> = str_in.chars().collect();
for start in 0..=chars.len() {
for end in (start..=chars.len()).rev() {
let sub: String = chars[start..end].iter().collect();
if !sub.is_empty() && matchpat(pat, &sub, true, true) {
let prefix: String = chars[..start].iter().collect();
let suffix: String = chars[end..].iter().collect();
return format!("{}{}{}", prefix, repl, suffix);
}
}
}
str_in.to_string()
}
pub fn _arguments(args: &[String]) -> i32 {
let words0 = getaparam("words").unwrap_or_default();
let cmd = words0.first().cloned().unwrap_or_default(); let oldcontext = getsparam("curcontext").unwrap_or_default();
let mut subopts: Vec<String> = Vec::new(); let mut singopt: Vec<String> = Vec::new(); let mut usecc = false; let mut rawret = false; let mut setnormarg = false; let mut optarg = false; let mut alwopt = String::new(); let mut opt_args_use_nul_separators: i32 = 0;
let mut i = 0usize;
while i < args.len() {
let a = &args[i];
let b = a.as_bytes();
let matches_flag = b.len() >= 2
&& b[0] == b'-'
&& (matches!(b[1], b'A' | b'M' | b'O')
|| (b.len() == 2 && matches!(b[1], b'0' | b'C' | b'R' | b'S' | b'W' | b'n' | b's' | b'w')));
if !matches_flag {
break;
}
match a.as_str() {
"-0" => {
opt_args_use_nul_separators = 1;
i += 1;
}
"-C" => {
usecc = true;
i += 1;
}
"-O" if i + 1 < args.len() => {
subopts = getaparam(&args[i + 1]).unwrap_or_default();
i += 2;
}
"-R" => {
rawret = true;
i += 1;
}
"-n" => {
setnormarg = true;
let _ = setiparam("NORMARG", -1);
i += 1;
}
"-w" => {
optarg = true;
i += 1;
}
"-W" => {
alwopt = "arg".to_string();
i += 1;
}
"-s" | "-S" => {
singopt.push(a.clone());
i += 1;
}
"-A" | "-M" if i + 1 < args.len() => {
singopt.push(a.clone());
singopt.push(args[i + 1].clone());
i += 2;
}
_ if b[1] == b'O' => {
subopts = getaparam(&a[2..]).unwrap_or_default();
i += 1;
}
_ if b[1] == b'A' || b[1] == b'M' => {
singopt.push(a.clone());
i += 1;
}
_ => break,
}
}
if i < args.len() && args[i] == ":" {
i += 1;
}
singopt.push(":".to_string());
if matches!(getsparam("PREFIX").as_deref(), Some("-") | Some("+")) {
alwopt = "arg".to_string();
}
let full: Vec<String> = args[i..].to_vec();
let specs: Vec<String> = match full.iter().rposition(|s| s == "--") {
Some(long) => long_option_cache(&full, long, &cmd),
None => full,
};
let autod = {
let ctx = format!(":completion:{}:options", oldcontext);
match zstyletab.lock() {
Ok(t) => t
.get(&ctx, "auto-description")
.and_then(|v| v.first().cloned())
.unwrap_or_default(),
Err(_) => String::new(),
}
};
if specs.is_empty() {
return 1; }
{
let mut init_argv: Vec<&str> = vec!["-i", autod.as_str()];
for s in &singopt {
init_argv.push(s);
}
for s in &specs {
init_argv.push(s);
}
let rc_init = comparguments(&init_argv);
tracing::debug!(target: "compsys_args", rc_init, nspecs = specs.len(), "comparguments -i");
if rc_init != 0 {
return 1;
}
}
let mut noargs = String::new();
let mut aret = false;
let mut tried = false;
let mut ret = 1i32;
let mut matched = false;
let mut hasopts = false;
let mut mesg = false;
let mut opts = false;
let mut local_set = false; let origpre = getsparam("PREFIX").unwrap_or_default();
let origipre = getsparam("IPREFIX").unwrap_or_default();
let nm = nmatches();
let mut have_descrs = false;
let rc_d = comparguments(&["-D", "descrs", "actions", "subcs"]);
tracing::debug!(target: "compsys_args", rc_d, prefix = %origpre, "comparguments -D");
if rc_d == 0 {
have_descrs = true;
let subcs = getaparam("subcs").unwrap_or_default();
let rc_o = comparguments(&["-O", "next", "direct", "odirect", "equal"]);
tracing::debug!(
target: "compsys_args",
rc_o,
next = ?getaparam("next").unwrap_or_default(),
direct = ?getaparam("direct").unwrap_or_default(),
"comparguments -O"
);
if rc_o == 0 {
opts = true;
let mut targ = subcs.clone();
targ.push("options".to_string());
let _ = _tags(&targ); } else {
let _ = _tags(&subcs); }
} else {
if comparguments(&["-a"]) == 0 {
noargs = "no more arguments".to_string();
} else {
noargs = "no arguments".to_string();
}
let orc = comparguments(&["-O", "next", "direct", "odirect", "equal"]);
if orc == 0 {
opts = true;
let _ = _tags(&["options".to_string()]); } else if orc == 2 {
let word = prefix_suffix();
let _ = bin_compadd(
"compadd",
&["-Q".to_string(), "-".to_string(), word],
&make_ops(),
0,
);
return 0;
} else {
let _ = _message(&[noargs.clone()]); return 1;
}
}
let nul_sep = opt_args_use_nul_separators.to_string();
let _ = comparguments(&["-M", "matcher"]);
let matcher = getsparam("matcher").unwrap_or_default();
let mut context: Vec<String> = Vec::new();
let mut state: Vec<String> = Vec::new();
let mut state_descr: Vec<String> = Vec::new();
setaparam("context", Vec::new());
setaparam("state", Vec::new());
setaparam("state_descr", Vec::new());
loop {
while _tags(&[]) == 0 {
if !tried && have_descrs {
let descrs = getaparam("descrs").unwrap_or_default();
let actions = getaparam("actions").unwrap_or_default();
let subcs = getaparam("subcs").unwrap_or_default();
let mut anum = 0usize; while anum < descrs.len() {
let action = actions.get(anum).cloned().unwrap_or_default();
let descr = descrs.get(anum).cloned().unwrap_or_default();
let subc = subcs.get(anum).cloned().unwrap_or_default();
anum += 1;
if subc.starts_with("argument") && setnormarg {
let _ = comparguments(&["-n", "NORMARG"]); }
if !matched && _requested(std::slice::from_ref(&subc)) != 0 {
continue;
}
let _ = setsparam(
"curcontext",
&format!("{}:{}", strip_last_field(&oldcontext), subc),
);
let _ = _description(&[subc.clone(), "expl".to_string(), descr.clone()]);
let mut action = action;
if let Some(rest) = action.strip_prefix("= ") {
action = rest.to_string();
let mut w = getaparam("words").unwrap_or_default();
w.insert(0, subc.clone());
setaparam("words", w);
let cur = getiparam("CURRENT");
let _ = setiparam("CURRENT", cur + 1);
}
if let Some(st) = action.strip_prefix("->") {
let st = st.trim().to_string();
if !state.iter().any(|s| s == &st) {
let _ = comparguments(&[
"-W",
"line",
"opt_args",
nul_sep.as_str(),
]);
state.push(st.clone());
state_descr.push(descr.clone());
setaparam("state", state.clone());
setaparam("state_descr", state_descr.clone());
if usecc {
let _ = setsparam(
"curcontext",
&format!("{}:{}", strip_last_field(&oldcontext), subc),
);
} else {
context.push(subc.clone());
setaparam("context", context.clone());
}
set_compstate_str("restore", ""); aret = true; }
continue; }
if !local_set {
local_set = true;
}
let _ = comparguments(&[
"-W",
"line",
"opt_args",
nul_sep.as_str(),
]);
if action.chars().all(|c| c == ' ') {
let _ = _message(&["-e".to_string(), subc.clone(), descr.clone()]);
mesg = true;
tried = true;
if alwopt.is_empty() {
alwopt = "yes".to_string();
}
} else if action.starts_with("((") && action.ends_with("))") {
let body = &action[2..action.len() - 2];
let ws: Vec<String> =
body.split_whitespace().map(|s| s.to_string()).collect();
setaparam("ws", ws);
let mut dv = vec![
"-t".to_string(),
subc.clone(),
descr.clone(),
"ws".to_string(),
"-M".to_string(),
matcher.clone(),
];
dv.extend(subopts.iter().cloned());
if dispatch_function_call("_describe", &dv).unwrap_or(1) != 0
&& alwopt.is_empty()
{
alwopt = "yes".to_string();
}
tried = true;
} else if action.starts_with('(') && action.ends_with(')') {
let body = &action[1..action.len() - 1];
let ws: Vec<String> =
body.split_whitespace().map(|s| s.to_string()).collect();
setaparam("ws", ws);
let mut av = vec![subc.clone(), "expl".to_string(), descr.clone(), "compadd".to_string()];
av.extend(subopts.iter().cloned());
av.push("-a".to_string());
av.push("-".to_string());
av.push("ws".to_string());
if _all_labels(&av) != 0 && alwopt.is_empty() {
alwopt = "yes".to_string();
}
tried = true;
} else if action.starts_with('{') && action.ends_with('}') {
let body = &action[1..action.len() - 1];
loop {
if _next_label(&[subc.clone(), "expl".to_string(), descr.clone()]) != 0 {
break;
}
if execute_script(body).map(|rc| rc == 0).unwrap_or(false) {
ret = 0;
}
}
if ret != 0 && alwopt.is_empty() {
alwopt = "yes".to_string();
}
tried = true;
} else if action.starts_with(' ') {
let parts: Vec<String> =
action.split_whitespace().map(|s| s.to_string()).collect();
if let Some((cmd, rest)) = parts.split_first() {
loop {
if _next_label(&[subc.clone(), "expl".to_string(), descr.clone()])
!= 0
{
break;
}
if dispatch_function_call(cmd, rest).unwrap_or(1) == 0 {
ret = 0;
}
}
}
if ret != 0 && alwopt.is_empty() {
alwopt = "yes".to_string();
}
tried = true;
} else {
let parts: Vec<String> =
action.split_whitespace().map(|s| s.to_string()).collect();
if let Some((cmd, rest)) = parts.split_first() {
loop {
if _next_label(&[subc.clone(), "expl".to_string(), descr.clone()])
!= 0
{
break;
}
let expl = getaparam("expl").unwrap_or_default();
let mut call_argv: Vec<String> = subopts.clone();
call_argv.extend(expl);
call_argv.extend(rest.iter().cloned());
let rc = if cmd == "compadd" {
bin_compadd("compadd", &call_argv, &make_ops(), 0)
} else {
dispatch_function_call(cmd, &call_argv).unwrap_or(1)
};
if rc == 0 {
ret = 0;
}
}
}
if ret != 0 && alwopt.is_empty() {
alwopt = "yes".to_string();
}
tried = true;
}
}
}
let prefix_needed_ok = {
let ctx = format!(":completion:{}:options", strip_last_field(&oldcontext));
let t = zstyletab
.lock()
.ok()
.map(|tab| tab.test_bool(&ctx, "prefix-needed"))
.unwrap_or(None);
let prefix_needed_true = t != Some(false); !prefix_needed_true
|| origpre.starts_with(|c: char| c == '-' || c == '+')
|| (!aret && !mesg && !tried) };
let cur_prefix = getsparam("PREFIX").unwrap_or_default();
if _requested(&["options".to_string()]) == 0
&& !hasopts
&& !matched
&& (!aret || cur_prefix == origpre)
&& prefix_needed_ok
{
let prevpre = getsparam("PREFIX").unwrap_or_default();
let previpre = getsparam("IPREFIX").unwrap_or_default();
let prevcontext = getsparam("curcontext").unwrap_or_default();
let _ = setsparam(
"curcontext",
&format!("{}:options", strip_last_field(&oldcontext)),
);
hasopts = true;
let _ = setsparam("PREFIX", &origpre); let _ = setsparam("IPREFIX", &origipre);
let want_single = alwopt.is_empty() || !tried || alwopt == "arg";
if want_single && comparguments(&["-s", "single"]) == 0 {
let single = getsparam("single").unwrap_or_default();
let word = prefix_suffix();
if single == "direct" {
let _ = _all_labels(&[
"options".to_string(),
"expl".to_string(),
"option".to_string(),
"compadd".to_string(),
"-QS".to_string(),
"".to_string(),
"-".to_string(),
word,
]);
} else if !optarg && single == "next" {
let _ = _all_labels(&[
"options".to_string(),
"expl".to_string(),
"option".to_string(),
"compadd".to_string(),
"-Q".to_string(),
"-".to_string(),
word,
]);
} else if single == "equal" {
let _ = _all_labels(&[
"options".to_string(),
"expl".to_string(),
"option".to_string(),
"compadd".to_string(),
"-QqS=".to_string(),
"-".to_string(),
word,
]);
} else {
let next = getaparam("next").unwrap_or_default();
let direct = getaparam("direct").unwrap_or_default();
let odirect = getaparam("odirect").unwrap_or_default();
let equal = getaparam("equal").unwrap_or_default();
let mut tmp1: Vec<String> = Vec::new();
tmp1.extend(next.iter().cloned());
tmp1.extend(direct.iter().cloned());
tmp1.extend(odirect.iter().cloned());
tmp1.extend(equal.iter().cloned());
let pfx = getsparam("PREFIX").unwrap_or_default();
if let Some(c0) = pfx.chars().next() {
if c0 == '-' || c0 == '+' {
tmp1.retain(|s| s.starts_with(c0));
}
}
if single == "next" {
if let Some(last) = pfx.chars().last() {
tmp1.retain(|s| {
let bytes: Vec<char> = s.chars().collect();
if bytes.len() >= 2
&& (bytes[0] == '-' || bytes[0] == '+')
&& bytes[1] == last
{
!(bytes.len() == 2
|| s[2..].starts_with(':'))
} else {
true
}
});
}
}
if !pfx.starts_with("--") {
tmp1.retain(|s| !s.starts_with("--"));
}
let tmp3: Vec<String> = tmp1
.iter()
.filter(|s| {
let c: Vec<char> = s.chars().collect();
c.len() >= 3
&& (c[0] == '-' || c[0] == '+')
&& c[2] != ':'
})
.cloned()
.collect();
tmp1.retain(|s| {
let c: Vec<char> = s.chars().collect();
c.len() >= 2
&& (c[0] == '-' || c[0] == '+')
&& (c.len() == 2 || c[2] == ':')
});
let tmp2: Vec<String> = tmp1
.iter()
.filter_map(|s| {
let name = s.splitn(2, ':').next().unwrap_or("");
let bare = name
.strip_prefix(|c: char| c == '-' || c == '+')
.unwrap_or(name);
if bare.chars().count() == 1 {
Some(format!("{}{}", pfx, bare))
} else {
None
}
})
.collect();
setaparam("tmp1", tmp1.clone());
setaparam("tmp2", tmp2);
setaparam("tmp3", tmp3);
let _ = dispatch_function_call(
"_describe",
&[
"-O".to_string(),
"option".to_string(),
"tmp1".to_string(),
"tmp2".to_string(),
"-S".to_string(),
"".to_string(),
"--".to_string(),
"tmp3".to_string(),
],
);
if optarg && single == "next" && nm == nmatches() {
let _ = _all_labels(&[
"options".to_string(),
"expl".to_string(),
"option".to_string(),
"compadd".to_string(),
"-Q".to_string(),
"-".to_string(),
prefix_suffix(),
]);
}
}
let _ = setsparam("single", "yes"); } else {
let mut next = getaparam("next").unwrap_or_default();
let odirect = getaparam("odirect").unwrap_or_default();
next.extend(odirect.iter().cloned()); setaparam("next", next);
let nm_before = nmatches();
let drc = dispatch_function_call(
"_describe",
&[
"-O".to_string(),
"option".to_string(),
"next".to_string(),
"-M".to_string(),
matcher.clone(),
"--".to_string(),
"direct".to_string(),
"-S".to_string(),
"".to_string(),
"-M".to_string(),
matcher.clone(),
"--".to_string(),
"equal".to_string(),
"-qS=".to_string(),
"-M".to_string(),
matcher.clone(),
],
);
tracing::debug!(
target: "compsys_args",
?drc,
nm_before,
nm_after = nmatches(),
"_describe -O option (multi)"
);
}
let _ = setsparam("PREFIX", &prevpre);
let _ = setsparam("IPREFIX", &previpre);
let _ = setsparam("curcontext", &prevcontext);
}
if tried {
let val = if !alwopt.is_empty() {
origpre.clone()
} else {
getsparam("PREFIX").unwrap_or_default()
};
if !val.starts_with(|c: char| c == '-' || c == '+') {
break;
}
}
}
if opts
&& !aret
&& !matched
&& (!tried || !alwopt.is_empty())
&& nm == nmatches()
{
let _ = setsparam("PREFIX", &origpre);
let _ = setsparam("IPREFIX", &origipre);
let full_pre = getsparam("PREFIX").unwrap_or_default();
let prefix = match full_pre.find('=') {
Some(p) => full_pre[p + 1..].to_string(),
None => full_pre.clone(),
};
let suffix = getsparam("SUFFIX").unwrap_or_default(); let pre_head = match full_pre.find('=') {
Some(p) => full_pre[..p].to_string(),
None => full_pre.clone(),
};
let _ = setsparam("PREFIX", &pre_head);
let _ = setsparam("SUFFIX", "");
let equal = getaparam("equal").unwrap_or_default();
let stripped = strip_colon_desc(&equal);
let mut cav: Vec<String> = vec![
"-M".to_string(),
matcher.clone(),
"-D".to_string(),
"equal".to_string(),
"-".to_string(),
];
cav.extend(stripped);
let _ = bin_compadd("compadd", &cav, &make_ops(), 0);
let equal = getaparam("equal").unwrap_or_default();
if equal.len() == 1 {
let opt_name = equal[0].splitn(2, ':').next().unwrap_or("").to_string();
let _ = setsparam("PREFIX", &prefix); let _ = setsparam("SUFFIX", &suffix); let cur_ipre = getsparam("IPREFIX").unwrap_or_default();
let _ = setsparam("IPREFIX", &format!("{}{}=", cur_ipre, opt_name));
matched = true;
let _ = comparguments(&["-L", opt_name.as_str(), "descrs", "actions", "subcs"]);
have_descrs = true;
let subcs = getaparam("subcs").unwrap_or_default();
let _ = _tags(&subcs); continue; }
}
break; }
if !aret || !usecc {
let _ = setsparam("curcontext", &oldcontext);
}
let final_rc: i32 = if aret {
if rawret {
300 } else {
if nm != nmatches() {
0
} else {
1
}
}
} else {
if !noargs.is_empty() && nm == nmatches() {
let _ = _message(&[noargs.clone()]);
}
if nm != nmatches() {
0
} else {
1
}
};
for p in [
"descrs", "actions", "subcs", "next", "direct", "odirect", "equal", "matcher", "single",
"line", "opt_args", "tmp1", "tmp2", "tmp3", "ws", "expl",
] {
unsetparam(p);
}
final_rc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_spec_list_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(_arguments(&[]), 1);
}
#[test]
fn flags_only_still_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(_arguments(&["-s".to_string(), "-C".to_string()]), 1);
}
#[test]
fn spec_without_completion_context_returns_one() {
let _g = crate::test_util::global_state_lock();
setaparam("words", vec!["cmd".to_string(), "".to_string()]);
let _ = setiparam("CURRENT", 2);
let r = _arguments(&["1:file:_files".to_string()]);
assert_eq!(r, 1);
}
#[test]
fn double_dash_drives_the_long_option_cache() {
let _g = crate::test_util::global_state_lock();
setaparam(
"words",
vec!["zshrs-no-such-cmd-xyz".to_string(), "".to_string()],
);
let _ = setiparam("CURRENT", 2);
let r = _arguments(&[
"-v[verbose]".to_string(),
"--".to_string(),
"-i".to_string(),
"ignored".to_string(),
]);
assert_eq!(r, 1);
}
#[test]
fn cache_name_sanitizes_command_word() {
assert_eq!(
sanitize_cache_name("_args_cache_/usr/bin/ls"),
"_args_cache__usr_bin_ls"
);
assert_eq!(sanitize_cache_name("a.b-c"), "a_b_c");
}
#[test]
fn word_char_helpers() {
assert_eq!(strip_to_wordchars("--foo=[bar]"), "--foobar");
assert_eq!(strip_trailing_nonword("--foo[=BAR]"), "--foo[=BAR");
assert_eq!(strip_trailing_nonword("--foo=BAR"), "--foo=BAR");
}
#[test]
fn desc_transform_maps_colon_and_brackets() {
assert_eq!(desc_transform("a:b [c] d"), "a-b (c) d");
}
#[test]
fn split_spec_pattern_splits_at_first_unescaped_colon() {
assert_eq!(
split_spec_pattern("*=FILE*:file:_files"),
("*=FILE*".to_string(), ":file:_files".to_string())
);
assert_eq!(
split_spec_pattern("*: : "),
("*".to_string(), ": : ".to_string())
);
assert_eq!(
split_spec_pattern("a\\:b:c"),
("a:b".to_string(), ":c".to_string())
);
}
#[test]
fn strip_leading_argword_drops_space_arg_space_space() {
assert_eq!(strip_leading_argword(" fooarg Do stuff"), "Do stuff");
assert_eq!(strip_leading_argword(" fooarg Do stuff"), " fooarg Do stuff");
}
#[test]
fn split_last_colon_uses_last_colon() {
assert_eq!(
split_last_colon("a:b:c"),
("a:b".to_string(), Some("c".to_string()))
);
assert_eq!(split_last_colon("abc"), ("abc".to_string(), None));
}
#[test]
fn arg_name_lower_extracts_lowercased_argname() {
assert_eq!(arg_name_lower("--foo=BAR"), "bar");
assert_eq!(arg_name_lower("--foo[=BAR]"), "bar");
}
#[test]
fn subst_first_replaces_leftmost_glob_match() {
let _g = crate::test_util::global_state_lock();
assert_eq!(subst_first("--enable-foo", "enable", "disable"), "--disable-foo");
assert_eq!(subst_first("--other", "enable", "disable"), "--other");
}
#[test]
fn scrape_help_lopts_extracts_long_options() {
let _g = crate::test_util::global_state_lock();
let help = "\
Usage: foo [OPTION]...
-v, --verbose be verbose
-o, --output=FILE write to FILE
--color[=WHEN] colorize output
";
let lopts = scrape_help_lopts(help);
assert!(lopts.contains(&"-v:be verbose".to_string()), "{:?}", lopts);
assert!(lopts.contains(&"--verbose:be verbose".to_string()), "{:?}", lopts);
assert!(lopts.contains(&"--output=FILE:write to FILE".to_string()), "{:?}", lopts);
assert!(
lopts.contains(&"--color[=WHEN]:colorize output".to_string()),
"{:?}",
lopts
);
}
#[test]
fn scrape_help_lopts_handles_description_on_next_line() {
let _g = crate::test_util::global_state_lock();
let help = " --long-only\n the description here\n";
let lopts = scrape_help_lopts(help);
assert!(
lopts.contains(&"--long-only:the description here".to_string()),
"{:?}",
lopts
);
}
#[test]
fn scrape_help_lopts_bracket_variant_expands() {
let _g = crate::test_util::global_state_lock();
let help = " --[fetch]all fetch everything\n";
let lopts = scrape_help_lopts(help);
assert!(lopts.contains(&"--fetchall:fetch everything".to_string()), "{:?}", lopts);
assert!(lopts.contains(&"--all:fetch everything".to_string()), "{:?}", lopts);
}
#[test]
fn has_unclosed_bracket_detects_bad_dup_pattern() {
assert!(has_unclosed_bracket("--color[=WHEN"));
assert!(!has_unclosed_bracket("--color[=WHEN]"));
assert!(!has_unclosed_bracket("--foo"));
}
#[test]
fn scrape_help_lopts_multi_option_with_bracketed_optarg() {
let _g = crate::test_util::global_state_lock();
let help = " -c, --color[=WHEN] colorize output\n";
let lopts = scrape_help_lopts(help);
assert!(lopts.contains(&"-c:colorize output".to_string()), "{:?}", lopts);
assert!(
lopts.contains(&"--color[=WHEN]:colorize output".to_string()),
"{:?}",
lopts
);
}
#[test]
fn nul_separator_flag_is_parsed() {
let _g = crate::test_util::global_state_lock();
assert_eq!(_arguments(&["-0".to_string()]), 1);
}
#[test]
fn strip_colon_desc_removes_description() {
assert_eq!(
strip_colon_desc(&["-v:verbose".to_string(), "-x".to_string()]),
vec!["-v".to_string(), "-x".to_string()]
);
}
#[test]
fn strip_last_field_drops_trailing_context_field() {
assert_eq!(strip_last_field(":completion::cmd:opt"), ":completion::cmd");
assert_eq!(strip_last_field("nocolon"), "nocolon");
}
}