use crate::ported::modules::zutil::lookupstyle;
use crate::ported::params::{getaparam, getsparam, setaparam, setsparam, unsetparam};
use crate::ported::zle::compcore::get_compstate_str;
use crate::ported::zle::complete::bin_compadd;
use crate::ported::zsh_h::{options, MAX_OPS};
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn before_first(s: &str, sep: &str) -> String {
match s.find(sep) {
Some(i) => s[..i].to_string(),
None => s.to_string(),
}
}
fn before_last(s: &str, sep: &str) -> String {
match s.rfind(sep) {
Some(i) => s[..i].to_string(),
None => s.to_string(),
}
}
fn after_first(s: &str, sep: &str) -> String {
match s.find(sep) {
Some(i) => s[i + sep.len()..].to_string(),
None => s.to_string(),
}
}
fn after_last(s: &str, sep: &str) -> String {
match s.rfind(sep) {
Some(i) => s[i + sep.len()..].to_string(),
None => s.to_string(),
}
}
fn strip_trailing(s: &str, sep: &str) -> String {
s.strip_suffix(sep).unwrap_or(s).to_string()
}
fn dedup(v: Vec<String>) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::with_capacity(v.len());
for e in v {
if seen.insert(e.clone()) {
out.push(e);
}
}
out
}
fn q_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_alphanumeric()
|| matches!(c, '@' | '%' | '+' | '=' | ':' | ',' | '.' | '/' | '-' | '_')
{
out.push(c);
} else {
out.push('\\');
out.push(c);
}
}
out
}
fn zstyle_t(style: &str, value: &str) -> bool {
let curcontext = getsparam("curcontext").unwrap_or_default();
let ctx = format!(":completion:{}:", curcontext);
lookupstyle(&ctx, style).iter().any(|v| v == value)
}
#[derive(Default)]
struct ParsedOpts {
sopts: Vec<String>,
group: Vec<String>,
expl: Vec<String>,
opts: Vec<String>,
matcher: Vec<String>,
imm: Vec<String>,
rest: Vec<String>,
}
#[derive(Clone, Copy)]
enum Dest {
Sopts,
Group,
Expl,
Opts,
Matcher,
Imm,
}
impl ParsedOpts {
fn dest(&mut self, d: Dest) -> &mut Vec<String> {
match d {
Dest::Sopts => &mut self.sopts,
Dest::Group => &mut self.group,
Dest::Expl => &mut self.expl,
Dest::Opts => &mut self.opts,
Dest::Matcher => &mut self.matcher,
Dest::Imm => &mut self.imm,
}
}
}
struct Spec {
flag: char,
takes_arg: bool,
plus: bool,
dest: Dest,
}
#[rustfmt::skip]
const SPECS: &[Spec] = &[
Spec { flag: 'J', takes_arg: true, plus: true, dest: Dest::Group },
Spec { flag: 'V', takes_arg: true, plus: true, dest: Dest::Group },
Spec { flag: 'x', takes_arg: true, plus: true, dest: Dest::Expl },
Spec { flag: 'X', takes_arg: true, plus: true, dest: Dest::Expl },
Spec { flag: 'P', takes_arg: true, plus: false, dest: Dest::Opts },
Spec { flag: 'F', takes_arg: true, plus: false, dest: Dest::Opts },
Spec { flag: 'S', takes_arg: true, plus: false, dest: Dest::Sopts },
Spec { flag: 'r', takes_arg: true, plus: false, dest: Dest::Sopts },
Spec { flag: 'R', takes_arg: true, plus: false, dest: Dest::Sopts },
Spec { flag: 'q', takes_arg: false, plus: false, dest: Dest::Sopts },
Spec { flag: '1', takes_arg: false, plus: false, dest: Dest::Sopts },
Spec { flag: '2', takes_arg: false, plus: false, dest: Dest::Sopts },
Spec { flag: 'o', takes_arg: true, plus: true, dest: Dest::Sopts },
Spec { flag: 'n', takes_arg: false, plus: false, dest: Dest::Sopts },
Spec { flag: 'f', takes_arg: false, plus: false, dest: Dest::Opts },
Spec { flag: 'M', takes_arg: true, plus: true, dest: Dest::Matcher },
Spec { flag: 'i', takes_arg: false, plus: false, dest: Dest::Imm },
];
fn zparse(args: &[String]) -> ParsedOpts {
let mut out = ParsedOpts::default();
let mut slot: std::collections::HashMap<char, usize> = std::collections::HashMap::new();
let mut i = 0usize;
'words: while i < args.len() {
let word = &args[i];
if word == "--" || word == "-" {
i += 1;
break;
}
if !word.starts_with('-') || word.chars().count() < 2 {
break;
}
let chars: Vec<char> = word.chars().collect();
let mut eaten = 1usize;
let mut c = 1usize;
while c < chars.len() {
let flag = chars[c];
let Some(spec) = SPECS.iter().find(|s| s.flag == flag) else {
break 'words;
};
if spec.takes_arg {
let attached: String = chars[c + 1..].iter().collect();
let value = if !attached.is_empty() {
attached
} else if i + 1 < args.len() {
eaten = 2;
args[i + 1].clone()
} else {
return ParsedOpts {
rest: args.to_vec(),
..Default::default()
};
};
let dest = out.dest(spec.dest);
match slot.get(&flag) {
Some(&at) if !spec.plus => dest[at + 1] = value,
_ => {
slot.entry(flag).or_insert(dest.len());
dest.push(format!("-{}", flag));
dest.push(value);
}
}
break;
}
let dest = out.dest(spec.dest);
if spec.plus || !slot.contains_key(&flag) {
slot.entry(flag).or_insert(dest.len());
dest.push(format!("-{}", flag));
}
c += 1;
}
i += eaten;
}
out.rest = args[i..].to_vec();
out
}
fn resolve_array(spec: &str) -> Vec<String> {
if !spec.starts_with('(') {
return getaparam(spec).unwrap_or_default();
}
let chars: Vec<char> = spec.chars().collect();
if chars.len() < 2 {
return Vec::new();
}
chars[1..chars.len() - 1]
.iter()
.collect::<String>()
.split_whitespace()
.map(|w| w.to_string())
.collect()
}
pub fn _multi_parts(args: &[String]) -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_multi_parts");
let comp_correct = getsparam("_comp_correct").unwrap_or_default();
let ParsedOpts {
mut sopts,
group,
expl,
opts,
matcher,
imm,
rest,
} = zparse(args);
sopts.extend(opts.iter().cloned());
let matcher: String = if matcher.len() >= 2 {
matcher[1].clone()
} else {
String::new()
};
if rest.len() < 2 {
return 1;
}
let sep = rest[0].clone();
let mut tmp1: Vec<String> = dedup(resolve_array(&rest[1]));
let mut pre = getsparam("PREFIX").unwrap_or_default();
let mut suf = getsparam("SUFFIX").unwrap_or_default();
let opre = pre.clone();
let osuf = suf.clone();
let orig = format!("{}{}", pre, suf);
let insert = get_compstate_str("insert").unwrap_or_default();
let pattern_match = get_compstate_str("pattern_match").unwrap_or_default();
let insert_is_menu = insert.ends_with("menu")
|| insert
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false);
let menu = insert_is_menu
|| !comp_correct.is_empty()
|| (!pattern_match.is_empty() && orig != q_quote(&orig));
let mut pref = String::new();
let mut cpre = String::new();
let matchspec = format!("r:|{}=* r:|=* {}", sep, matcher);
setaparam("tmp1", tmp1.clone());
{
let argv = vec![
"-O".to_string(),
"matches".into(),
"-M".into(),
matchspec.clone(),
"-a".into(),
"tmp1".into(),
];
let _ = bin_compadd("compadd", &argv, &make_ops(), 0);
}
let mut matches: Vec<String> = dedup(getaparam("matches").unwrap_or_default());
if matches.is_empty() {
matches = tmp1.clone();
}
macro_rules! cleanup {
() => {{
let _ = unsetparam("tmp1");
let _ = unsetparam("matches");
}};
}
loop {
let (mprefix, msuffix): (String, String) = if pre.contains(&sep) {
(before_first(&pre, &sep), String::new())
} else {
(pre.clone(), before_first(&suf, &sep))
};
let _ = setsparam("PREFIX", &mprefix);
let _ = setsparam("SUFFIX", &msuffix);
let exact_pat = format!("{}{}{}", mprefix, msuffix, sep);
if !format!("{}{}", mprefix, msuffix).is_empty() || pre.starts_with(&sep) {
tmp1 = matches
.iter()
.filter(|m| m.starts_with(&exact_pat))
.cloned()
.collect();
} else {
tmp1 = Vec::new();
}
tmp1 = dedup(tmp1);
let npref: String;
if !tmp1.is_empty() {
npref = format!("{}{}{}", mprefix, msuffix, sep);
} else {
let words: Vec<String> = matches
.iter()
.map(|m| before_first(m, &sep))
.filter(|w| !w.is_empty())
.collect();
let mut argv: Vec<String> = vec![
"-O".into(),
"tmp1".into(),
"-M".into(),
matchspec.clone(),
"-".into(),
];
argv.extend(words.iter().cloned());
let _ = bin_compadd("compadd", &argv, &make_ops(), 0);
tmp1 = dedup(getaparam("tmp1").unwrap_or_default());
if tmp1.is_empty() && !comp_correct.is_empty() {
let _ = bin_compadd("compadd", &argv, &make_ops(), 0);
tmp1 = dedup(getaparam("tmp1").unwrap_or_default());
}
if tmp1.len() == 1 {
if format!("{}{}", pre, suf).contains(&sep) {
npref = format!("{}{}", tmp1[0], sep);
} else {
matches = matches
.iter()
.filter(|m| m.starts_with(&tmp1[0]))
.cloned()
.collect();
matches = dedup(matches);
let _ = setsparam("PREFIX", &format!("{}{}", cpre, pre));
let _ = setsparam("SUFFIX", &suf);
let rc;
if (!imm.is_empty() && matches.len() == 1) || zstyle_t("expand", "suffix") {
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.extend(sopts.iter().cloned());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
for m in &matches {
argv.push(format!("{}{}", pref, m));
}
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
} else {
let has_multi = matches
.iter()
.any(|m| m.starts_with(&format!("{}{}", tmp1[0], sep)));
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
if has_multi {
argv.push("-p".into());
argv.push(pref.clone());
argv.push("-r".into());
argv.push(sep.clone());
argv.push("-S".into());
argv.push(sep.clone());
argv.extend(opts.iter().cloned());
} else {
argv.push("-p".into());
argv.push(pref.clone());
argv.extend(sopts.iter().cloned());
}
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(tmp1[0].clone());
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
}
cleanup!();
return rc;
}
} else if !tmp1.is_empty() {
let mut ret = 1i32;
let _ = setsparam("PREFIX", &pre);
let _ = setsparam("SUFFIX", &suf);
setaparam("matches", matches.clone());
{
let argv = vec![
"-O".to_string(),
"matches".into(),
"-M".into(),
matchspec.clone(),
"-a".into(),
"matches".into(),
];
let _ = bin_compadd("compadd", &argv, &make_ops(), 0);
}
matches = dedup(getaparam("matches").unwrap_or_default());
if pre.contains(&sep) {
let _ = setsparam("PREFIX", &format!("{}{}", cpre, before_first(&pre, &sep)));
let _ = setsparam(
"SUFFIX",
&format!("{}{}{}", sep, after_first(&pre, &sep), suf),
);
} else {
let _ = setsparam("PREFIX", &format!("{}{}", cpre, pre));
let _ = setsparam("SUFFIX", &suf);
}
if !format!("{}{}", pre, suf).is_empty() {
matches = matches
.iter()
.filter(|m| tmp1.iter().any(|t| m.starts_with(t)))
.cloned()
.collect();
matches = dedup(matches);
}
if !zstyle_t("expand", "suffix") || menu || insert.is_empty() {
let presuf = format!("{}{}", pre, suf);
let tmp2: Vec<String> = if presuf.contains(&sep) {
vec![
"-s".into(),
format!("{}{}", sep, after_first(&presuf, &sep)),
]
} else {
Vec::new()
};
let w1: Vec<String> = matches
.iter()
.filter(|m| m.ends_with(&sep))
.map(|m| before_first(m, &sep))
.filter(|w| !w.is_empty())
.collect();
if compadd_seg(&group, &expl, &sep, &opts, &pref, &tmp2, &matchspec, &w1) == 0 {
ret = 0;
}
if matches.iter().any(|m| m.starts_with(&sep)) {
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-S".into());
argv.push(String::new());
argv.extend(opts.iter().cloned());
argv.push("-p".into());
argv.push(pref.clone());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(sep.clone());
if bin_compadd("compadd", &argv, &make_ops(), 0) == 0 {
ret = 0;
}
}
let w2: Vec<String> = matches
.iter()
.filter(|m| has_interior_sep(m, &sep))
.map(|m| before_first(m, &sep))
.filter(|w| !w.is_empty())
.collect();
if compadd_seg(&group, &expl, &sep, &opts, &pref, &tmp2, &matchspec, &w2) == 0 {
ret = 0;
}
let w3: Vec<String> = matches
.iter()
.filter(|m| !m.contains(&sep))
.cloned()
.collect();
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-S".into());
argv.push(String::new());
argv.extend(opts.iter().cloned());
argv.push("-p".into());
argv.push(pref.clone());
argv.extend(tmp2.iter().cloned());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.extend(w3);
if bin_compadd("compadd", &argv, &make_ops(), 0) == 0 {
ret = 0;
}
} else {
let w1: Vec<String> = matches
.iter()
.filter(|m| m.contains(&sep))
.map(|m| before_first(m, &sep))
.filter(|w| !w.is_empty())
.collect();
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.extend(opts.iter().cloned());
argv.push("-p".into());
argv.push(pref.clone());
argv.push("-s".into());
argv.push(String::new()); argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.extend(w1);
if bin_compadd("compadd", &argv, &make_ops(), 0) == 0 {
ret = 0;
}
let w2: Vec<String> = matches
.iter()
.filter(|m| !m.contains(&sep))
.cloned()
.collect();
let mut argv2: Vec<String> = Vec::new();
argv2.extend(group.iter().cloned());
argv2.extend(expl.iter().cloned());
argv2.push("-S".into());
argv2.push(String::new());
argv2.extend(opts.iter().cloned());
argv2.push("-p".into());
argv2.push(pref.clone());
argv2.push("-M".into());
argv2.push(matchspec.clone());
argv2.push("-".into());
argv2.extend(w2);
if bin_compadd("compadd", &argv2, &make_ops(), 0) == 0 {
ret = 0;
}
}
cleanup!();
return ret;
} else {
if !zstyle_t("expand", "prefix") || orig == format!("{}{}{}", pref, pre, suf) {
cleanup!();
return 1;
}
let _ = setsparam("PREFIX", &format!("{}{}", cpre, pre));
let _ = setsparam("SUFFIX", &suf);
let rc;
if !suf.is_empty() {
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-s".into());
argv.push(suf.clone());
argv.extend(sopts.iter().cloned());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(format!("{}{}", pref, pre));
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
} else {
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-S".into());
argv.push(String::new());
argv.extend(opts.iter().cloned());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(format!("{}{}", pref, pre));
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
}
cleanup!();
return rc;
}
}
matches = matches
.iter()
.filter(|m| m.starts_with(&npref))
.map(|m| after_first(m, &sep))
.filter(|m| !m.is_empty())
.collect::<Vec<String>>();
matches = dedup(matches);
pref = format!("{}{}", pref, npref);
if pre.contains(&sep) {
cpre = format!("{}{}{}", cpre, before_first(&pre, &sep), sep);
pre = after_first(&pre, &sep);
} else if suf.contains(&sep) {
cpre = format!("{}{}{}", cpre, pre, before_first(&suf, &sep));
cpre.push_str(&sep);
pre = after_first(&suf, &sep);
suf = String::new();
} else {
let _ = setsparam("PREFIX", &format!("{}{}", opre, osuf));
let _ = setsparam("SUFFIX", "");
let rc;
if !pref.is_empty() && orig != pref {
if ends_with_sep_component(&pref, &sep) {
let p = format!("{}{}", before_last(&before_last(&pref, &sep), &sep), sep);
let word = after_last(&strip_trailing(&pref, &sep), &sep);
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.extend(opts.iter().cloned());
argv.push("-p".into());
argv.push(p);
argv.push("-S".into());
argv.push(sep.clone());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(word);
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
} else if pref.contains(&sep) {
let p = format!("{}{}", before_last(&pref, &sep), sep);
let word = after_last(&pref, &sep);
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-S".into());
argv.push(String::new());
argv.extend(opts.iter().cloned());
argv.push("-p".into());
argv.push(p);
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(word);
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
} else {
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-S".into());
argv.push(String::new());
argv.extend(opts.iter().cloned());
argv.push("-M".into());
argv.push(matchspec.clone());
argv.push("-".into());
argv.push(pref.clone());
rc = bin_compadd("compadd", &argv, &make_ops(), 0);
}
} else {
rc = 1;
}
cleanup!();
return rc;
}
}
}
#[allow(clippy::too_many_arguments)]
fn compadd_seg(
group: &[String],
expl: &[String],
sep: &str,
opts: &[String],
pref: &str,
tmp2: &[String],
matchspec: &str,
words: &[String],
) -> i32 {
let mut argv: Vec<String> = Vec::new();
argv.extend(group.iter().cloned());
argv.extend(expl.iter().cloned());
argv.push("-r".into());
argv.push(sep.to_string());
argv.push("-S".into());
argv.push(sep.to_string());
argv.extend(opts.iter().cloned());
argv.push("-p".into());
argv.push(pref.to_string());
argv.extend(tmp2.iter().cloned());
argv.push("-M".into());
argv.push(matchspec.to_string());
argv.push("-".into());
argv.extend(words.iter().cloned());
bin_compadd("compadd", &argv, &make_ops(), 0)
}
fn has_interior_sep(s: &str, sep: &str) -> bool {
let mut start = 0usize;
while let Some(rel) = s[start..].find(sep) {
let i = start + rel;
if i >= 1 && i + sep.len() <= s.len() - 1 {
return true;
}
start = i + sep.len();
if start > s.len() {
break;
}
}
false
}
fn ends_with_sep_component(pref: &str, sep: &str) -> bool {
if !pref.ends_with(sep) {
return false;
}
let head = &pref[..pref.len() - sep.len()];
head.contains(sep)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::params::setsparam;
#[test]
fn returns_one_with_no_matches() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("PREFIX", "xyz");
let _ = setsparam("SUFFIX", "");
let _r = _multi_parts(&["/".to_string(), "(foo/bar baz/qux)".to_string()]);
assert!(getaparam("tmp1").map(|v| v.is_empty()).unwrap_or(true));
assert!(getaparam("matches").map(|v| v.is_empty()).unwrap_or(true));
}
#[test]
fn zparse_routes_specs() {
let p = zparse(&[
"-J".into(),
"grp".into(),
"-P".into(),
"pfx".into(),
"-f".into(),
"-M".into(),
"mspec".into(),
"-i".into(),
"-S".into(),
"sf".into(),
"/".into(),
"(a/b c/d)".into(),
]);
assert_eq!(p.group, vec!["-J", "grp"]);
assert_eq!(p.opts, vec!["-P", "pfx", "-f"]);
assert_eq!(p.matcher, vec!["-M", "mspec"]);
assert_eq!(p.imm, vec!["-i"]);
assert_eq!(p.sopts, vec!["-S", "sf"]);
assert_eq!(p.rest, vec!["/", "(a/b c/d)"]);
}
#[test]
fn zparse_consumes_end_of_options_dash() {
let p = zparse(&[
"-f".into(),
"-J".into(),
"files".into(),
"-".into(),
"/".into(),
"arr".into(),
]);
assert_eq!(p.opts, vec!["-f"]);
assert_eq!(p.group, vec!["-J", "files"]);
assert_eq!(p.rest, vec!["/", "arr"]);
}
#[test]
fn zparse_consumes_double_dash() {
let p = zparse(&["-f".into(), "--".into(), "/".into(), "arr".into()]);
assert_eq!(p.opts, vec!["-f"]);
assert_eq!(p.rest, vec!["/", "arr"]);
}
#[test]
fn zparse_clusters_and_attached_values() {
let p = zparse(&["-qfi12".into(), "-Sxyz".into(), "/".into(), "arr".into()]);
assert_eq!(p.sopts, vec!["-q", "-1", "-2", "-S", "xyz"]);
assert_eq!(p.opts, vec!["-f"]);
assert_eq!(p.imm, vec!["-i"]);
assert_eq!(p.rest, vec!["/", "arr"]);
}
#[test]
fn zparse_replace_vs_append() {
let p = zparse(&[
"-R".into(),
"z".into(),
"-S".into(),
"a".into(),
"-r".into(),
"b".into(),
"-S".into(),
"c".into(),
"-R".into(),
"y".into(),
"/".into(),
"arr".into(),
]);
assert_eq!(p.sopts, vec!["-R", "y", "-S", "c", "-r", "b"]);
let p = zparse(&[
"-o".into(),
"1".into(),
"-S".into(),
"a".into(),
"-o".into(),
"2".into(),
"-S".into(),
"b".into(),
"/".into(),
"arr".into(),
]);
assert_eq!(p.sopts, vec!["-o", "1", "-S", "b", "-o", "2"]);
}
#[test]
fn zparse_unknown_letter_leaves_whole_word() {
let p = zparse(&["-fz".into(), "/".into(), "arr".into()]);
assert_eq!(p.opts, vec!["-f"]);
assert_eq!(p.rest, vec!["-fz", "/", "arr"]);
}
#[test]
fn literal_array_tests_only_the_leading_paren() {
assert_eq!(resolve_array("(a b c)"), vec!["a", "b", "c"]);
assert_eq!(resolve_array("(a b c"), vec!["a", "b"]);
assert!(resolve_array("(").is_empty());
assert!(resolve_array("()").is_empty());
}
#[test]
fn expansion_primitives() {
assert_eq!(before_first("a/b/c", "/"), "a");
assert_eq!(before_last("a/b/c", "/"), "a/b");
assert_eq!(after_first("a/b/c", "/"), "b/c");
assert_eq!(after_last("a/b/c", "/"), "c");
assert_eq!(strip_trailing("a/b/", "/"), "a/b");
assert!(has_interior_sep("a/b", "/"));
assert!(!has_interior_sep("/ab", "/"));
assert!(!has_interior_sep("ab/", "/"));
assert!(ends_with_sep_component("a/b/", "/"));
assert!(!ends_with_sep_component("a/", "/"));
}
}