use crate::lex::untokenize;
use crate::parse::{ShellWord, VarModifier, ZshParamFlag};
use crate::ported::exec::getoutput;
use crate::ported::glob::xpandbraces;
use crate::ported::hashnameddir::nameddirtab;
use crate::ported::hashtable::{aliastab_lock, cmdnamtab_lock, shfunctab_lock, sufaliastab_lock};
use crate::ported::hist::{
casemodify, hsubl, hsubpatopt, hsubr, rembutext, remlpaths, remtext, remtpath,
};
use crate::ported::math::mathevali;
use crate::ported::modules::parameter::*;
use crate::ported::options::{opt_state_set, ZSH_OPTIONS_SET};
use crate::ported::params::{
assignsparam, convbase_underscore, convfloat_underscore, getarrvalue, getsparam,
lookup_special_var, paramtab, paramtab_hashed_storage, setsparam,
};
use crate::ported::pattern::{patcompile, pattry};
use crate::ported::prompt::promptexpand;
use crate::ported::string::{dupstring, dyncat};
use crate::ported::utils::{
errflag, getkeystring, quotestring, xsymlinks, zerr, GETKEY_CTRL, GETKEY_EMACS,
GETKEY_OCTAL_ESC,
};
use crate::ported::zsh_h::PAT_HEAPDUP;
#[allow(unused_imports)]
use crate::ported::zsh_h::{
hashnode, isset, param, Bang, Bnull, Bnullkeep, Dash, Dnull, Equals, Hat, Inang, Inbrace,
Inbrack, Inpar, Inparmath, Marker, Nularg, Outang, OutangProc, Outbrace, Outbrack, Outpar,
Outparmath, Param, Pound, Qstring, Qtick, Quest, Snull, Star, Stringg, Tick, Tilde,
ALIAS_GLOBAL, ALIAS_SUFFIX, CASMOD_NONE, DISABLED, HASHED, HISTSUBSTPATTERN, IGNOREBRACES,
KSHTYPESET, LEXFLAGS_ACTIVE, LEXFLAGS_COMMENTS_KEEP, LEXFLAGS_COMMENTS_STRIP, LEXFLAGS_NEWLINE,
MN_FLOAT, MN_UNSET, MULTSUB_PARAM_NAME, MULTSUB_WS_AT_END, MULTSUB_WS_AT_START, PM_ARRAY,
PM_EFLOAT, PM_EXPORTED, PM_FFLOAT, PM_HASHED, PM_HIDE, PM_HIDEVAL, PM_INTEGER, PM_LEFT,
PM_LOWER, PM_NAMEREF, PM_READONLY, PM_RIGHT_B, PM_RIGHT_Z, PM_SPECIAL, PM_TAGGED, PM_TIED,
PM_UNIQUE, PM_UPPER, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_NOSHWORDSPLIT,
PREFORK_NO_UNTOK, PREFORK_SHWORDSPLIT, PREFORK_SINGLE, PREFORK_SPLIT, PREFORK_SUBEXP,
PREFORK_TYPESET, PUSHDMINUS, QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_DOLLARS, QT_NONE,
QT_QUOTEDZPUTS, QT_SINGLE, QT_SINGLE_OPTIONAL, RCEXPANDPARAM, SCANPM_NONAMEREF,
SCANPM_WANTKEYS, SCANPM_WANTVALS, SHFILEEXPANSION, SHWORDSPLIT, SORTIT_ANYOLDHOW,
SORTIT_BACKWARDS, SORTIT_IGNORING_CASE, SORTIT_NUMERICALLY, SORTIT_NUMERICALLY_SIGNED,
SORTIT_SOMEHOW, SUB_ALL, SUB_BIND, SUB_DOSUBST, SUB_EGLOB, SUB_EIND, SUB_END, SUB_GLOBAL,
SUB_LEN, SUB_LIST, SUB_LONG, SUB_MATCH, SUB_REST, SUB_RETFAIL, SUB_START, SUB_SUBSTR,
};
use crate::zsh_h::{CASMOD_CAPS, CASMOD_LOWER, CASMOD_UPPER};
use crate::DPUTS;
#[allow(unused_imports)]
use std::ffi::CString;
use std::sync::atomic::{AtomicUsize, Ordering};
pub const LF_ARRAY: u32 = 1;
fn keyvalpairelement(list: &mut LinkList, node_idx: usize) -> Option<usize> {
let data = list.getdata(node_idx)?.to_string(); let chars: Vec<char> = data.chars().collect();
if chars.is_empty() || (chars[0] != Inbrack && chars[0] != '[')
{
return None; }
let mut end_pos: Option<usize> = None; for (i, &c) in chars.iter().enumerate().skip(1) {
if c == Outbrack || c == ']' {
end_pos = Some(i); break; }
}
let end_pos = end_pos?;
if end_pos + 1 >= chars.len() {
return None; }
let is_append = chars.get(end_pos + 1) == Some(&'+') && (chars.get(end_pos + 2) == Some(&Equals)
|| chars.get(end_pos + 2) == Some(&'='));
let is_assign = !is_append && (chars.get(end_pos + 1) == Some(&Equals)
|| chars.get(end_pos + 1) == Some(&'='));
if !is_assign && !is_append {
return None;
}
let raw_key: String = chars[1..end_pos].iter().collect(); let key_subst = singsub(&raw_key); let key = untokenize(&key_subst);
let value_start = if is_append { end_pos + 3 } else { end_pos + 2 }; let raw_value: String = chars[value_start..].iter().collect(); let value_subst = singsub(&raw_value); let value = untokenize(&value_subst);
let marker = if is_append {
format!("{}+", Marker) } else {
Marker.to_string() };
list.setdata(node_idx, marker); let key_idx = list.insertlinknode(node_idx, key); let val_idx = list.insertlinknode(key_idx, value);
Some(val_idx) }
pub fn prefork(list: &mut LinkList, flags: i32, ret_flags: &mut i32) {
let mut node_idx = 0; let mut stop_idx: Option<usize> = None; let mut keep = false; let asssub = (flags & PREFORK_TYPESET != 0) && isset(KSHTYPESET); let mut iter_count = 0u32;
while node_idx < list.nodes.len() {
iter_count += 1; if iter_count > 100_000 {
return; } if (flags & (PREFORK_SINGLE | PREFORK_ASSIGN)) == PREFORK_ASSIGN {
if let Some(new_idx) = keyvalpairelement(list, node_idx) {
node_idx = new_idx + 1; *ret_flags |= PREFORK_KEY_VALUE;
continue; } }
if errflag_set() {
return; }
if isset(SHFILEEXPANSION) {
if let Some(data) = list.getdata(node_idx) {
let new_data = filesub(
data, flags & (PREFORK_TYPESET | PREFORK_ASSIGN), ); list.setdata(node_idx, new_data); } } else {
if let Some(new_idx) = stringsubst(
list, node_idx, flags & !(PREFORK_TYPESET | PREFORK_ASSIGN), ret_flags, asssub, ) {
node_idx = new_idx; } else {
return; } }
node_idx += 1; }
if isset(SHFILEEXPANSION) {
node_idx = 0; while node_idx < list.nodes.len() {
if let Some(new_idx) = stringsubst(
list, node_idx, flags & !(PREFORK_TYPESET | PREFORK_ASSIGN), ret_flags, asssub, ) {
node_idx = new_idx + 1; } else {
return; } } }
node_idx = 0; while node_idx < list.nodes.len() {
if Some(node_idx) == stop_idx {
keep = false; }
if let Some(data) = list.getdata(node_idx) {
if !data.is_empty() {
let mut s = data.to_string();
crate::ported::glob::remnulargs(&mut s);
if s == "\u{a1}" {
s.clear();
}
let data = s;
list.setdata(node_idx, data.clone());
if !isset(IGNOREBRACES) && (flags & PREFORK_SINGLE == 0) {
if !keep {
stop_idx = list.nextnode(node_idx); }
loop {
let cur = match list.getdata(node_idx) {
Some(d) => d.to_string(),
None => break,
};
let expanded = xpandbraces(&cur, false); if expanded.len() <= 1 {
break;
} keep = true; list.setdata(node_idx, expanded[0].clone()); let mut last = node_idx;
for ex in &expanded[1..] {
last = list.insertlinknode(last, ex.clone());
}
}
}
if !isset(SHFILEEXPANSION) && !SKIP_FILESUB.with(|c| c.get()) {
if let Some(data) = list.getdata(node_idx) {
let new_data = filesub(
data, flags & (PREFORK_TYPESET | PREFORK_ASSIGN), ); list.setdata(node_idx, new_data); } } } else if (flags & PREFORK_SINGLE == 0) && (*ret_flags & PREFORK_KEY_VALUE == 0) && !keep
{
list.delete_node(node_idx); continue; } }
if errflag_set() {
return; }
node_idx += 1; } }
fn stringsubstquote(strstart: &str, pstrdpos: usize) -> (String, usize) {
let chars: Vec<char> = strstart.chars().collect();
let start = pstrdpos + 2; let mut end = start; let mut escaped = false;
while end < chars.len() {
if escaped {
escaped = false; end += 1; continue; }
if chars[end] == '\\' {
escaped = true; end += 1; continue; }
if chars[end] == '\'' {
break;
} end += 1;
}
let content: String = chars[start..end].iter().collect();
let (strsub, _) = getkeystring(&content);
let prefix: String = chars[..pstrdpos].iter().collect(); let suffix: String = if end + 1 < chars.len() {
chars[end + 1..].iter().collect() } else {
String::new() };
let strret = if strsub.is_empty() && prefix.is_empty() && suffix.is_empty() {
Nularg.to_string() } else {
format!("{}{}{}", prefix, strsub, suffix) };
let new_pos = prefix.chars().count()
+ strret
.chars()
.count()
.saturating_sub(prefix.chars().count() + suffix.chars().count());
(strret, new_pos) }
fn stringsubst(
list: &mut LinkList, node_idx: usize, pf_flags: i32, ret_flags: &mut i32, asssub: bool, ) -> Option<usize> {
let mut str3 = list.getdata(node_idx)?.to_string(); let mut pos = 0;
let mut p1_iter = 0u32; loop {
if errflag_set() {
break; } p1_iter += 1; if p1_iter > 100_000 {
return None; } let chars: Vec<char> = str3.chars().collect(); if pos >= chars.len() {
break; } let c = chars[pos];
if (c == Inang || c == OUTANGPROC || (pos == 0 && c == Equals)) && chars.get(pos + 1) == Some(&Inpar)
{
if errflag_set() {
return None; } let start = pos; pos += 2; let mut depth = 1_i32; while pos < chars.len() && depth > 0 {
let ch = chars[pos]; if ch == Inpar {
depth += 1;
}
else if ch == Outpar {
depth -= 1;
} pos += 1; } let str_chars: Vec<char> = str3.chars().collect(); let mut new_str = String::with_capacity(str_chars.len());
new_str.extend(str_chars[..start].iter()); new_str.extend(str_chars[pos..].iter()); str3 = new_str; list.setdata(node_idx, str3.clone()); pos = start; continue; }
pos += 1; }
pos = 0; let mut iter_count = 0u32; loop {
if errflag_set() {
break; } iter_count += 1; if iter_count > 100_000 {
return None; } let chars: Vec<char> = str3.chars().collect(); if pos >= chars.len() {
break; } let c = chars[pos];
if c == '\u{9d}' {
let mut end = pos + 1; while end < chars.len() && chars[end] != '\u{9d}' {
end += 1; } let prefix: String = chars[..pos].iter().collect(); let body: String = chars[pos + 1..end].iter().collect(); let suffix: String = if end < chars.len() {
chars[end + 1..].iter().collect() } else {
String::new() }; str3 = format!("{}{}{}", prefix, body, suffix); pos += body.chars().count(); list.setdata(node_idx, str3.clone()); continue; } if c == '\u{9e}' {
let prefix: String = chars[..pos].iter().collect(); let suffix: String = if pos + 1 < chars.len() {
chars[pos + 1..].iter().collect() } else {
String::new() }; str3 = format!("{}{}", prefix, suffix); list.setdata(node_idx, str3.clone()); continue; } if c == '\u{9f}' && pos + 1 < chars.len() {
let prefix: String = chars[..pos].iter().collect(); let kept = chars[pos + 1]; let suffix: String = if pos + 2 < chars.len() {
chars[pos + 2..].iter().collect() } else {
String::new() }; str3 = format!("{}{}{}", prefix, kept, suffix); pos += 1; list.setdata(node_idx, str3.clone()); continue; } if c == '\'' {
let mut end = pos + 1; while end < chars.len() && chars[end] != '\'' {
end += 1; } let prefix: String = chars[..pos].iter().collect(); let body: String = chars[pos + 1..end].iter().collect(); let suffix: String = if end < chars.len() {
chars[end + 1..].iter().collect() } else {
String::new() }; str3 = format!("{}{}{}", prefix, body, suffix); pos += body.chars().count(); list.setdata(node_idx, str3.clone()); continue; }
let qt = c == Qstring; if qt || c == STRING || c == '$' {
let next_c = chars.get(pos + 1).copied(); let next_is = |tok: char, lit: char| {
next_c == Some(tok) || next_c == Some(lit) };
if next_c == Some(Inparmath) || (next_c == Some('(') && chars.get(pos + 2).copied() == Some('('))
{
let token_shape = next_c == Some(Inparmath);
let mixed_shape = token_shape && chars.get(pos + 2).copied() == Some('(');
let start = if mixed_shape {
pos + 3
} else if token_shape {
pos + 2
} else {
pos + 3
};
let mut depth = if token_shape && !mixed_shape {
1_i32
} else {
2_i32
};
let mut p = start;
let mut end_off: Option<usize> = None;
let mut end_outparmath = false;
while p < chars.len() {
let ch = chars[p];
if ch == Inparmath {
depth += 1;
} else if ch == Outparmath {
depth -= 1;
if depth == 0 {
end_off = Some(p);
end_outparmath = true;
break;
}
} else if ch == '(' || ch == Inpar {
depth += 1;
} else if ch == ')' || ch == Outpar {
depth -= 1;
if depth == 0 {
end_off = Some(p);
break;
}
}
p += 1;
}
if let Some(end) = end_off {
let expr_end = if end_outparmath {
let mut e = end;
if e > start && chars[e - 1] == ')' {
e -= 1;
}
e
} else {
end - 1
};
let expr: String = chars[start..expr_end].iter().collect();
let prefix: String = chars[..pos].iter().collect();
let suffix: String = if end + 1 < chars.len() {
chars[end + 1..].iter().collect()
} else {
String::new()
};
let result_only = arithsubst(&expr, "", "");
str3 = format!("{}{}{}", prefix, result_only, suffix);
list.setdata(node_idx, str3.clone());
pos = prefix.chars().count() + result_only.chars().count();
continue;
}
}
if next_is(Inpar, '(') || next_is(Inparmath, '\0') {
if !qt {
list.flags |= LF_ARRAY; } let cmd_open = pos + 1; let chars: Vec<char> = str3.chars().collect(); let mut depth = 0_i32; let mut end = cmd_open; while end < chars.len() {
let ch = chars[end]; if ch == '(' || ch == Inpar {
depth += 1;
}
else if ch == ')' || ch == Outpar {
depth -= 1; if depth == 0 {
break;
} } end += 1; } if end < chars.len() && depth == 0 {
let cmd: String = chars[cmd_open + 1..end].iter().collect(); let trimmed = cmd.trim_start();
let output = if let Some(rest) = trimmed.strip_prefix('<') {
let path = singsub(rest.trim());
std::fs::read_to_string(path.trim()).unwrap_or_default()
} else {
getoutput(&cmd, 1).join("")
};
let prefix: String = chars[..pos].iter().collect(); let suffix: String = if end + 1 < chars.len() {
chars[end + 1..].iter().collect() } else {
String::new() }; str3 = format!("{}{}{}", prefix, output.trim_end_matches('\n'), suffix); pos = prefix.chars().count() + output.trim_end_matches('\n').chars().count(); list.setdata(node_idx, str3.clone()); } else {
pos += 1; } continue; } else if next_is(Inbrack, '[') {
let start = pos + 2; let open = if next_c == Some(Inbrack) {
Inbrack
} else {
'['
}; let close = if open == Inbrack { Outbrack } else { ']' }; let chars: Vec<char> = str3.chars().collect(); let mut end_off: Option<usize> = None; let tail_only_tokenized = open == Inbrack
&& chars[start.saturating_sub(1)..]
.iter()
.all(|&c| c != '[' && c != ']');
if tail_only_tokenized {
let open_byte_off: usize =
chars[..start - 1].iter().map(|c| c.len_utf8()).sum();
let mut cursor: &str = &str3[open_byte_off..];
let bal = crate::ported::utils::skipparens(Inbrack, Outbrack, &mut cursor);
if bal == 0 {
let after_close_byte = str3.len() - cursor.len();
let after_close_char = str3[..after_close_byte].chars().count();
end_off = Some(after_close_char.saturating_sub(1).saturating_sub(start));
}
} else {
let mut depth = 1_i32;
let mut p = start;
while p < chars.len() {
let ch = chars[p];
if ch == open || ch == '[' {
depth += 1;
} else if ch == close || ch == ']' {
depth -= 1;
if depth == 0 {
end_off = Some(p - start);
break;
}
}
p += 1;
}
}
if let Some(end) = end_off {
let expr: String = chars[start..start + end].iter().collect(); let prefix: String = chars[..pos].iter().collect(); let suffix: String = if start + end + 1 < chars.len() {
chars[start + end + 1..].iter().collect()
} else {
String::new()
};
let result_only = arithsubst(&expr, "", ""); str3 = format!("{}{}{}", prefix, result_only, suffix); list.setdata(node_idx, str3.clone()); pos = prefix.chars().count() + result_only.chars().count(); continue; } else {
errflag_set_error(); zerr("closing bracket missing"); return None; } } else if next_c == Some(Snull) || next_c == Some('\'') {
let (new_str, new_pos) = stringsubstquote(&str3, pos); str3 = new_str; pos = new_pos; list.setdata(node_idx, str3.clone()); continue; } else {
let mut new_pf_flags = pf_flags; if (isset(SHWORDSPLIT) && (pf_flags & PREFORK_NOSHWORDSPLIT == 0)) || (pf_flags & PREFORK_SPLIT != 0)
{
new_pf_flags |= PREFORK_SHWORDSPLIT; }
IN_PARAMSUBST_NEST.with(|c| c.set(c.get() + 1)); let (new_str, new_pos, new_nodes) = paramsubst(
&str3, pos, qt, new_pf_flags & (PREFORK_SINGLE | PREFORK_SHWORDSPLIT | PREFORK_SUBEXP), ret_flags, ); IN_PARAMSUBST_NEST.with(|c| c.set(c.get() - 1)); if errflag_set() {
return None; }
if new_nodes.is_empty() {
list.setdata(node_idx, String::new()); } else {
let mut current_idx = node_idx; for (i, node_data) in new_nodes.into_iter().enumerate() {
if i == 0 {
list.setdata(current_idx, node_data); } else {
current_idx = list.insertlinknode(current_idx, node_data);
} } }
str3 = list.getdata(node_idx)?.to_string(); pos = new_pos; continue; } }
let qt = c == Qtick; if qt || c == Tick || c == '`' {
if !qt {
list.flags |= LF_ARRAY; } let chars: Vec<char> = str3.chars().collect(); let cmd_start = pos + 1; let mut end = cmd_start; while end < chars.len()
&& chars[end] != Tick
&& chars[end] != Qtick
&& chars[end] != '`'
{
if chars[end] == '\\' && end + 1 < chars.len() {
end += 1;
} end += 1; } if end < chars.len() {
let cmd: String = chars[cmd_start..end].iter().collect(); let output = getoutput(&cmd, 1).join("");
let prefix: String = chars[..pos].iter().collect(); let suffix: String = if end + 1 < chars.len() {
chars[end + 1..].iter().collect() } else {
String::new() }; str3 = format!("{}{}{}", prefix, output.trim_end_matches('\n'), suffix); pos = prefix.chars().count() + output.trim_end_matches('\n').chars().count(); list.setdata(node_idx, str3.clone()); } else {
pos += 1; } continue; }
if asssub && (c == '=' || c == Equals) && pos > 0 { }
pos += 1; }
if errflag_set() {
None } else {
Some(node_idx) } }
pub fn quotesubst(str: &str) -> String {
let mut result = str.to_string(); let mut pos = 0_usize;
loop {
let chars: Vec<char> = result.chars().collect(); if pos >= chars.len() {
break;
} if pos + 1 < chars.len() && chars[pos] == STRING && chars[pos + 1] == Snull
{
let (new_str, new_pos) = stringsubstquote(&result, pos); result = new_str; pos = new_pos; } else {
pos += 1; } }
crate::ported::glob::remnulargs(&mut result);
result
}
pub fn globlist(list: &mut LinkList, flags: i32) {
crate::ported::glob::BADCSHGLOB.store(0, std::sync::atomic::Ordering::Relaxed);
let mut node_idx = 0;
while node_idx < list.nodes.len() && !errflag_set() {
let data = match list.getdata(node_idx) {
Some(d) => d.to_string(), None => {
node_idx += 1;
continue;
} };
if flags & PREFORK_KEY_VALUE != 0 && data.chars().next() == Some(Marker) {
node_idx += 3; continue; }
let no_untok = flags & PREFORK_NO_UNTOK != 0; let _ = no_untok; let expanded: Vec<String> = crate::ported::glob::glob_path(&data);
if expanded.is_empty() {
let nullglob = isset(crate::ported::zsh_h::NULLGLOB); let csh_nullglob = isset(crate::ported::zsh_h::CSHNULLGLOB); let has_glob_chars = data.chars().any(|c| matches!(c, '*' | '?' | '[' | ']'))
|| crate::ported::pattern::haswilds(&data);
if has_glob_chars && !nullglob && !csh_nullglob && isset(crate::ported::zsh_h::NOMATCH)
{
crate::ported::utils::zerr(&format!("no matches found: {}", data)); crate::ported::utils::errflag.fetch_or(
crate::ported::zsh_h::ERRFLAG_ERROR,
std::sync::atomic::Ordering::Relaxed,
); list.delete_node(node_idx);
continue;
}
node_idx += 1;
} else if expanded.len() == 1 {
list.setdata(node_idx, expanded.into_iter().next().unwrap());
node_idx += 1;
} else {
list.delete_node(node_idx);
for (i, p) in expanded.iter().enumerate() {
if i == 0 {
list.insert_at(node_idx, p.clone());
} else {
list.insertlinknode(node_idx + i - 1, p.clone());
}
}
node_idx += expanded.len(); }
}
let noerrs = crate::ported::exec::noerrs.load(std::sync::atomic::Ordering::Relaxed) != 0;
let badcshglob = crate::ported::glob::BADCSHGLOB.load(std::sync::atomic::Ordering::Relaxed);
if noerrs {
crate::ported::glob::BADCSHGLOB.store(0, std::sync::atomic::Ordering::Relaxed);
} else if badcshglob == 1 {
crate::ported::utils::zerr("no match"); }
}
pub fn singsub(s: &str) -> String {
let mut list = LinkList::default(); list.push_back(s.to_string()); let mut ret_flags = 0i32;
prefork(&mut list, PREFORK_SINGLE, &mut ret_flags);
if errflag_set() {
return String::new(); }
let result = list.getdata(0).cloned().unwrap_or_default(); DPUTS!(
list.nodes.len() > 1, "BUG: singsub() produced more than one word!"
); result }
pub fn multsub(s: &str, pf_flags: i32) -> (String, Vec<String>, bool, i32) {
let mut ms_flags = 0i32; let mut x = s.to_string();
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n\0".to_string()); let is_ifs_sep = |c: char| -> bool {
ifs.contains(c) };
if pf_flags & PREFORK_SPLIT != 0 {
let leading: usize = x.chars().take_while(|&c| is_ifs_sep(c)).count(); if leading > 0 {
ms_flags |= MULTSUB_WS_AT_START; x = x.chars().skip(leading).collect(); }
}
let mut list = LinkList::default(); list.push_back(x.clone());
if pf_flags & PREFORK_SPLIT != 0 {
let chars: Vec<char> = x.chars().collect(); let mut nodes: Vec<String> = Vec::new(); let mut cur = String::new(); let mut inq = false; let mut inp = 0_i32; let mut i = 0_usize; while i < chars.len() {
let c = chars[i]; let is_token = matches!(c as u32, 0x80..=0x9F); if c == '\u{99}' || c == '\u{9a}' {
cur.push(c); i += 1; if i < chars.len() {
cur.push(chars[i]); i += 1; }
continue; }
match c { Dnull | Snull | Tick => { inq = !inq; } Inpar => { inp += 1; } Outpar => { inp -= 1; } _ => {}
}
if !inq && inp == 0 && !is_token && is_ifs_sep(c) {
if !cur.is_empty() || nodes.is_empty() {
nodes.push(std::mem::take(&mut cur)); }
i += 1; while i < chars.len() && is_ifs_sep(chars[i]) {
i += 1; }
if i >= chars.len() {
ms_flags |= MULTSUB_WS_AT_END; break; }
continue; }
cur.push(c); i += 1; }
if !cur.is_empty() {
nodes.push(cur); }
list = LinkList::default(); for n in nodes {
list.push_back(n); }
}
let mut ret_flags = 0i32; prefork(&mut list, pf_flags, &mut ret_flags);
if errflag_set() {
return (String::new(), Vec::new(), false, ms_flags); }
let l = list.len(); if l > 1 || (list.flags & LF_ARRAY != 0) {
let arr: Vec<String> = list.iter().cloned().collect(); let join_sep = ifs.chars().next().map(String::from).unwrap_or_default(); let joined = arr.join(&join_sep); return (joined, arr, true, ms_flags); }
if l == 1 {
let result = list.getdata(0).cloned().unwrap_or_default(); return (result.clone(), vec![result], false, ms_flags); }
(String::new(), vec![String::new()], false, ms_flags) }
fn filesub(namptr: &str, assign: i32) -> String {
let mut namptr: String = filesubstr(namptr, assign != 0).unwrap_or_else(|| namptr.to_string());
if assign == 0 {
return namptr; }
let mut eql: Option<usize> = None;
if assign & PREFORK_TYPESET != 0 {
if namptr.len() >= 2 {
if let Some(sub) = namptr[1..].find('=').map(|p| p + 1) {
eql = Some(sub); let str_start = sub + 1; if str_start < namptr.len() && (namptr.as_bytes()[str_start] == b'~'
|| namptr.as_bytes()[str_start] == b'=')
{
let rhs = &namptr[str_start..]; if let Some(expanded) = filesubstr(rhs, true) {
namptr = format!("{}{}", &namptr[..str_start], expanded);
} } } else {
return namptr; } } else {
return namptr; } }
let mut ptr_off = 0_usize; loop {
let slice = &namptr[ptr_off..]; let colon_rel = match slice.find(':') {
Some(p) => p, None => break, }; let sub = ptr_off + colon_rel; let str_start = sub + 1; let len = sub; let past_eql = match eql {
Some(e) => sub > e, None => true, }; let starts_with_tilde_or_equals = if str_start < namptr.len() {
let suffix = &namptr[str_start..];
let first = suffix.chars().next();
matches!(first, Some('~') | Some('=') | Some('\u{98}') | Some(Equals))
} else {
false
};
if past_eql && starts_with_tilde_or_equals {
let rhs = &namptr[str_start..]; if let Some(expanded) = filesubstr(rhs, true) {
namptr = format!("{}{}", &namptr[..str_start], expanded); } } ptr_off = len + 1; if ptr_off >= namptr.len() {
break; } } namptr }
pub fn equalsubstr(s: &str, assign: bool, nomatch: bool) -> Option<String> {
let end = s .chars() .take_while(|&c| {
c != '\0' && c != Inpar && !(assign && c == ':') })
.count();
let cmdstr_raw: String = s.chars().take(end).collect(); let mut cmdstr = untokenize(&cmdstr_raw); crate::ported::glob::remnulargs(&mut cmdstr);
let cnam = crate::ported::builtin::findcmd(&cmdstr, 1, 0);
match cnam {
Some(path) => {
if end < s.chars().count() {
let rest: String = s.chars().skip(end).collect(); Some(format!("{}{}", path, rest)) } else {
Some(path) }
}
None => {
if nomatch {
zerr(&format!("{}: not found", cmdstr)); }
None }
}
}
pub fn filesubstr(namptr: &str, assign: bool) -> Option<String> {
if namptr.is_empty() {
return None; }
let chars: Vec<char> = namptr.chars().collect(); let first = chars[0];
if first == '\u{98}'
{
if chars.len() == 1 {
let home = getsparam("HOME").unwrap_or_default();
return Some(home);
}
let raw_nx = chars[1];
let nx = if raw_nx == '\u{9b}' { '-' } else { raw_nx };
if nx == '=' {
return None;
}
let isend =
|c: char| -> bool { c == '\0' || c == '/' || c == Inpar || (assign && c == ':') };
if isend(nx) {
let home = getsparam("HOME").unwrap_or_default();
let suffix: String = chars[1..].iter().collect();
return Some(format!("{}{}", home, suffix));
}
if nx == '+' && (chars.len() == 2 || isend(chars[2])) {
let pwd = getsparam("PWD").unwrap_or_default();
let suffix: String = chars[2..].iter().collect();
return Some(format!("{}{}", pwd, suffix));
}
if nx == '-' && (chars.len() == 2 || isend(chars[2])) {
let oldpwd = getsparam("OLDPWD")
.or_else(|| getsparam("PWD"))
.unwrap_or_default();
let suffix: String = chars[2..].iter().collect();
return Some(format!("{}{}", oldpwd, suffix));
}
if (nx == '+' || nx == '-' || nx.is_ascii_digit()) && !nx.is_whitespace() {
let mut p = 1_usize;
let neg = chars[p] == '-';
if chars[p] == '+' || chars[p] == '-' {
p += 1;
}
let dstart = p;
while p < chars.len() && chars[p].is_ascii_digit() {
p += 1;
}
if p > dstart && (p == chars.len() || isend(chars[p])) {
let val: i32 = chars[dstart..p]
.iter()
.collect::<String>()
.parse()
.unwrap_or(0);
let val = if neg { -val } else { val };
let pwd = getsparam("PWD").unwrap_or_default();
let dirstack: Vec<String> = DIRSTACK.lock().map(|d| d.clone()).unwrap_or_default();
let pushdminus = isset(PUSHDMINUS); let entry = dstackent(
if neg { '-' } else { '+' }, val, &dirstack, &pwd, pushdminus, );
if let Some(dir) = entry {
let suffix: String = chars[p..].iter().collect();
return Some(format!("{}{}", dir, suffix));
}
return None;
}
}
let mut p = 1_usize;
while p < chars.len() && (chars[p].is_ascii_alphanumeric() || chars[p] == '_') {
p += 1;
}
if p > 1 && (p == chars.len() || isend(chars[p])) {
let user: String = chars[1..p].iter().collect();
let suffix: String = chars[p..].iter().collect();
let named = nameddirtab()
.lock()
.ok()
.and_then(|t| t.get(&user).map(|nd| nd.dir.clone()));
if let Some(path) = named {
return Some(format!("{}{}", path, suffix));
}
if let Ok(cname) = CString::new(user.clone()) {
unsafe {
let pw = libc::getpwnam(cname.as_ptr());
if !pw.is_null() {
let home_ptr = (*pw).pw_dir;
if !home_ptr.is_null() {
let home = std::ffi::CStr::from_ptr(home_ptr)
.to_string_lossy()
.into_owned();
return Some(format!("{}{}", home, suffix));
}
}
}
}
zerr(&format!("no such user or named directory: {}", user));
errflag_set_error();
return None;
}
return None;
}
if first == Equals
&& chars.len() > 1
&& chars[1] != Inpar
&& crate::ported::zsh_h::isset(crate::ported::zsh_h::EQUALSOPT)
{
let cmd_part: String = chars[1..].iter().collect();
let cmd = if assign {
cmd_part.split(':').next().unwrap_or(&cmd_part).to_string()
} else {
cmd_part.clone()
};
let path = getsparam("PATH").unwrap_or_default();
for dir in path.split(':') {
let full = format!("{}/{}", dir, cmd);
if std::path::Path::new(&full).exists() {
if assign && cmd_part.len() > cmd.len() {
let suffix = &cmd_part[cmd.len()..];
return Some(format!("{}{}", full, suffix));
}
return Some(full);
}
}
zerr(&format!("{}: not found", cmd));
errflag_set_error();
}
None
}
pub fn strcatsub(prefix: &str, src: &str, suffix: &str, glob_subst: bool) -> String {
if prefix.is_empty() && suffix.is_empty() {
if glob_subst {
}
return src.to_string(); }
let mut result = String::with_capacity(
prefix.len() + src.len() + suffix.len() + 1,
);
result.push_str(prefix); result.push_str(src); if glob_subst {
}
result.push_str(suffix); result }
pub fn wcpadwidth(wc: char, multi_width: i32) -> i32 {
let wcw = crate::ported::utils::zwcwidth(wc) as i32;
match multi_width {
0 => 1, 1 => {
if wcw >= 0 {
wcw
} else {
0
}
} _ => {
if wcw > 0 {
1
} else {
0
}
} }
}
pub fn dopadding(
s: &str, prenum: usize, postnum: usize, preone: Option<&str>, postone: Option<&str>, premul: &str, postmul: &str, multi_width: i32, ) -> String {
let cells = |t: &str| -> usize {
if multi_width <= 0 {
t.chars().count() } else {
t.chars().map(|c| wcpadwidth(c, multi_width) as usize).sum() } };
let len = cells(s); let total_width = prenum + postnum;
if total_width == 0 || total_width == len {
return s.to_string(); }
let mut result = String::new();
if prenum > 0 {
let chars: Vec<char> = s.chars().collect();
if len > prenum {
let skip = len - prenum; result = chars.into_iter().skip(skip).collect(); } else {
let padding_needed = prenum - len;
if let Some(pre) = preone {
let pre_len = pre.chars().count(); if pre_len <= padding_needed {
let repeat_len = padding_needed - pre_len; if !premul.is_empty() {
let mul_len = premul.chars().count(); let full_repeats = repeat_len / mul_len; let partial = repeat_len % mul_len;
if partial > 0 {
result.extend(premul.chars().skip(mul_len - partial));
} for _ in 0..full_repeats {
result.push_str(premul); } } result.push_str(pre); } else {
result.extend(pre.chars().skip(pre_len - padding_needed)); } } else {
if !premul.is_empty() {
let mul_len = premul.chars().count(); let full_repeats = padding_needed / mul_len; let partial = padding_needed % mul_len;
if partial > 0 {
result.extend(premul.chars().skip(mul_len - partial)); } for _ in 0..full_repeats {
result.push_str(premul); } } }
result.push_str(s); } } else {
result = s.to_string(); }
if postnum > 0 {
let current_len = cells(&result);
if current_len > postnum {
result = result.chars().take(postnum).collect(); } else if current_len < postnum {
let padding_needed = postnum - current_len;
if let Some(post) = postone {
let post_len = post.chars().count(); if post_len <= padding_needed {
result.push_str(post); let remaining = padding_needed - post_len; if !postmul.is_empty() {
let mul_len = postmul.chars().count(); let full_repeats = remaining / mul_len; let partial = remaining % mul_len;
for _ in 0..full_repeats {
result.push_str(postmul); } if partial > 0 {
result.extend(postmul.chars().take(partial)); } } } else {
result.extend(post.chars().take(padding_needed)); } } else if !postmul.is_empty() {
let mul_len = postmul.chars().count(); let full_repeats = padding_needed / mul_len; let partial = padding_needed % mul_len;
for _ in 0..full_repeats {
result.push_str(postmul); } if partial > 0 {
result.extend(postmul.chars().take(partial)); } } } }
result }
pub fn get_strarg(s: &str) -> Option<(char, String, &str)> {
let mut iter = s.char_indices();
let (_, del) = iter.next()?;
let close_del = match del {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
Inpar => Outpar,
Inbrack => Outbrack,
Inbrace => Outbrace,
Inang => Outang,
_ => del,
};
let mut content = String::new();
let mut rest_start = s.len(); for (byte_off, c) in iter {
if c == close_del {
rest_start = byte_off + c.len_utf8();
break;
}
content.push(c);
}
Some((del, content, &s[rest_start..]))
}
pub fn get_intarg(s: &str) -> Option<(i64, &str)> {
let (_del, content, rest) = get_strarg(s)?;
if rest.is_empty() && content.is_empty() {
return None;
}
let parsed = subst_parse_str(&content, false, true)?;
let expanded = singsub(&parsed); if errflag_set() {
return None;
}
let ret = match mathevali(&expanded) {
Ok(n) => n, Err(msg) => {
zerr(&msg); return None; }
};
let abs_ret = if ret < 0 { -ret } else { ret };
Some((abs_ret, rest)) }
pub fn subst_parse_str(sp: &str, single: bool, err: bool) -> Option<String> {
let _ = err; let mut buf: String = sp.to_string();
if !single {
let mut chars: Vec<char> = buf.chars().collect(); let mut qt = false; for c in chars.iter_mut() {
if !qt {
if *c == Qstring {
*c = Stringg; } else if *c == Qtick {
*c = Tick; }
}
if *c == Dnull {
qt = !qt; }
}
buf = chars.iter().collect(); }
Some(buf) }
pub fn substevalchar(ptr: &str) -> Option<String> {
let ires = match mathevali(ptr) {
Ok(n) => n, Err(msg) => {
zerr(&msg);
return Some(String::new()); } }; if ires < 0 {
zerr("character not in range"); return Some(String::new()); }
if let Some(ch) = char::from_u32(ires as u32) {
let mut buf = [0u8; 4]; return Some(ch.encode_utf8(&mut buf).to_string()); }
let byte = (ires as u32 & 0xFF) as u8; Some(String::from_utf8_lossy(&[byte]).into_owned()) }
pub fn untok_and_escape(s: &str, escapes: bool, tok_arg: bool) -> String {
let mut dst: Option<String> = None;
let chars: Vec<char> = s.chars().collect(); if escapes && chars.len() >= 2 && (chars[0] == STRING || chars[0] == Qstring)
{
let mut pend = 1_usize; while pend < chars.len() {
let c = chars[pend]; if !(c.is_ascii_alphanumeric() || c == '_') {
break; }
pend += 1; }
if pend == chars.len() {
let name: String = chars[1..].iter().collect(); dst = vars_get(&name); }
}
let result = match dst {
Some(d) => d, None => {
let untoked = untokenize(s); if escapes {
getkeystring(&untoked).0 } else {
untoked }
}
};
if tok_arg {
}
result }
pub fn check_colon_subscript(s: &str) -> Option<(String, String)> {
if s.is_empty() || s.starts_with(|c: char| c.is_ascii_alphabetic()) || s.starts_with('&')
{
return None; }
if s.starts_with(':') {
return Some(("0".to_string(), s.to_string())); }
let chars: Vec<char> = s.chars().collect(); let mut depth: i32 = 0; let mut end: Option<usize> = None; for (i, &c) in chars.iter().enumerate() {
match c {
'[' | Inbrack => depth += 1, ']' | Outbrack => depth -= 1, '(' | Inpar => depth += 1, ')' | Outpar => depth -= 1, ':' if depth == 0 => {
end = Some(i);
break;
} _ => {}
}
}
let end = end.unwrap_or(s.len()); let expr: String = chars[..end].iter().collect();
let parsed = subst_parse_str(&expr, false, true)?; let mut expanded = singsub(&parsed); if errflag_set() {
return None;
} crate::ported::glob::remnulargs(&mut expanded); let untoked = untokenize(&expanded);
let rest: String = chars[end..].iter().collect(); Some((untoked, rest)) }
pub fn paramsubst(
s: &str, start_pos: usize, qt: bool, pf_flags: i32, ret_flags: &mut i32, ) -> (String, usize, Vec<String>) {
let chars: Vec<char> = s.chars().collect(); let mut pos = start_pos + 1; let mut result_nodes = Vec::new();
let c = chars.get(pos).copied().unwrap_or('\0');
if c == Inbrace || c == '{' {
pos += 1; let mut depth = 1_i32; let mut end = pos; while end < chars.len() && depth > 0 {
let ch = chars[end]; if ch == '{' || ch == Inbrace {
depth += 1;
}
else if ch == '}' || ch == Outbrace {
depth -= 1; if depth == 0 {
break;
} } end += 1; } if end >= chars.len() || depth != 0 {
zerr("closing brace missing"); errflag_set_error(); return (String::new(), chars.len(), vec![]); }
let body: String = chars[pos..end].iter().collect(); let new_pos = if end < chars.len() { end + 1 } else { end };
let body_chars: Vec<char> = body.chars().collect();
let mut idx = 0_usize;
let mut isarr: i32 = 0;
let mut plan9 = isset(RCEXPANDPARAM);
let mut evalchar = false;
let mut whichlen: i32 = 0;
let mut wantt = false;
let mut sub_flags_bits: i32 = 0;
let mut flnum: u32 = 0;
let mut sortit: i32 = SORTIT_ANYOLDHOW; let mut indord: i32 = 0;
let mut unique = false;
let mut casmod: i32 = CASMOD_NONE;
let mut quotemod: i32 = 0; let mut quotetype: i32 = QT_NONE; let mut quoteerr = false;
let mut mods: i32 = 0;
let mut shsplit: i32 = 0;
let mut spsep: Option<String> = None; let mut sep: Option<String> = None;
let mut premul: Option<String> = None; let mut postmul: Option<String> = None; let mut preone: Option<String> = None; let mut postone: Option<String> = None;
let mut prenum: i64 = 0; let mut postnum: i64 = 0;
let mut multi_width: u32 = 0;
let mut arrasg: i32 = 0;
let mut eval = false;
let mut aspar = false;
let mut presc: i32 = 0;
let mut getkeys: i32 = -1;
let mut nojoin: i32 = if (pf_flags & PREFORK_SHWORDSPLIT) != 0 {
let ifs = vars_get("IFS").unwrap_or_default(); if ifs.is_empty() && !qt {
1
} else {
0
} } else {
0 };
let mut inbrace: i32 = 1; let _ = &mut inbrace;
let mut hkeys: u32 = 0;
let mut hvals: u32 = 0;
let mut subexp: i32 = 0;
let mut escapes: bool = false;
let mut subexp_array_temp: Option<String> = None; if matches!(body_chars.first(), Some(&'(') | Some(&Inpar)) {
let mut tok_arg = false; let mut d = 1_i32; idx = 1; if !body_chars.iter().skip(1).any(|c| *c == ')' || *c == Outpar) {
zerr("bad substitution"); errflag_set_error(); return (String::new(), new_pos, vec![]); } while idx < body_chars.len() && d > 0 {
let fc = body_chars[idx]; match fc {
c if c == '(' || c == Inpar => {
d += 1;
} c if c == ')' || c == Outpar => {
d -= 1;
if d == 0 {
idx += 1;
break;
}
} 'L' => {
casmod = CASMOD_LOWER; } 'U' => {
casmod = CASMOD_UPPER; } 'C' => {
casmod = CASMOD_CAPS; } 'q' => {
if quotetype == QT_DOLLARS || quotetype == QT_BACKSLASH_PATTERN {
zerr("error in flags");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
let next = body_chars.get(idx + 1).copied(); if next == Some('-') || next == Some('+') {
if quotemod != 0 {
zerr("error in flags");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
idx += 1; quotemod = 1; quotetype = if next == Some('+') {
QT_QUOTEDZPUTS } else {
QT_SINGLE_OPTIONAL }; } else {
if quotetype == QT_SINGLE_OPTIONAL {
zerr("error in flags");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
quotemod += 1; quotetype += 1; } } 'A' => {
arrasg += 1;
} '@' => {
nojoin = 2; } 'P' => {
aspar = true;
} 't' => {
wantt = true;
} '!' => {
if ((hkeys | hvals) & !SCANPM_NONAMEREF) != 0 {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
hkeys = SCANPM_NONAMEREF;
}
'k' => {
if (hkeys & !SCANPM_WANTKEYS) != 0 {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
hkeys = SCANPM_WANTKEYS;
} 'v' => {
if (hvals & !SCANPM_WANTVALS) != 0 {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
hvals = SCANPM_WANTVALS;
} '#' => {
evalchar = true;
} 'l' | 'r' => {
let is_left = fc == 'l'; idx += 1; if idx >= body_chars.len() {
break;
}
let del = body_chars[idx]; let close_del = match del {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
other => other,
};
idx += 1; let mut num_str = String::new();
while idx < body_chars.len() && body_chars[idx].is_ascii_digit() {
num_str.push(body_chars[idx]);
idx += 1;
}
let n: i64 = num_str.parse().unwrap_or(0); if idx < body_chars.len() && body_chars[idx] == close_del {
idx += 1;
}
if is_left {
prenum = n;
} else {
postnum = n;
} if idx >= body_chars.len() || body_chars[idx] != del {
continue;
}
idx += 1; let s1_start = idx;
while idx < body_chars.len() && body_chars[idx] != close_del {
idx += 1;
}
let s1_raw: String = body_chars[s1_start..idx].iter().collect();
let s1 = untok_and_escape(&s1_raw, escapes, tok_arg);
if is_left {
premul = Some(s1);
} else {
postmul = Some(s1);
}
if idx < body_chars.len() {
idx += 1; }
if idx >= body_chars.len() || body_chars[idx] != del {
continue;
}
idx += 1;
let s2_start = idx;
while idx < body_chars.len() && body_chars[idx] != close_del {
idx += 1;
}
let s2_raw: String = body_chars[s2_start..idx].iter().collect();
let s2 = untok_and_escape(&s2_raw, escapes, tok_arg);
if is_left {
preone = Some(s2);
} else {
postone = Some(s2);
}
if idx < body_chars.len() {
idx += 1;
}
continue; }
'o' => {
if sortit == 0 {
sortit |= SORTIT_SOMEHOW; } } 'O' => {
sortit |= SORTIT_BACKWARDS; } 'i' => {
sortit |= SORTIT_IGNORING_CASE; } 'n' => {
sortit |= SORTIT_NUMERICALLY; } '-' => {
sortit |= SORTIT_NUMERICALLY_SIGNED;
} 'a' => {
sortit |= SORTIT_SOMEHOW; indord = 1; } 'u' => {
unique = true;
} '_' => {
idx += 1;
if idx >= body_chars.len() {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
let del = body_chars[idx];
idx += 1;
let inner_start = idx;
while idx < body_chars.len() && body_chars[idx] != del {
idx += 1;
}
if inner_start < idx {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
if idx >= body_chars.len() {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
idx += 1;
continue;
} '*' => {
sub_flags_bits |= SUB_EGLOB;
} 'I' => {
idx += 1; let mut digits = String::new(); while idx < body_chars.len() && body_chars[idx].is_ascii_digit()
{
digits.push(body_chars[idx]); idx += 1; } if let Ok(n) = digits.parse::<u32>() {
flnum = n; } continue; } 'M' => {
sub_flags_bits |= SUB_MATCH;
} 'R' => {
sub_flags_bits |= SUB_REST;
} 'B' => {
sub_flags_bits |= SUB_BIND;
} 'E' => {
sub_flags_bits |= SUB_EIND;
} 'N' => {
sub_flags_bits |= SUB_LEN;
} 'S' => {
sub_flags_bits |= SUB_SUBSTR;
} 'e' => {
eval = true;
} 'Q' => {
quotemod -= 1; } 'X' => {
quoteerr = true;
} 'D' => {
mods |= 1; } 'V' => {
mods |= 2; } 'b' => {
quotemod = 1; quotetype = QT_BACKSLASH_PATTERN; } 'c' => {
whichlen = 1; } 'w' => {
whichlen = 2; } 'W' => {
whichlen = 3; } 'z' => {
shsplit = LEXFLAGS_ACTIVE; } 'Z' => {
shsplit = LEXFLAGS_ACTIVE; idx += 1; if idx >= body_chars.len() || body_chars[idx] == ')' {
zerr("bad substitution"); errflag_set_error();
return (String::new(), new_pos, vec![]);
}
let del = body_chars[idx]; idx += 1; let mut found_close = false;
while idx < body_chars.len() && body_chars[idx] != del
{
let ch = body_chars[idx]; if ch == 'c' {
shsplit |= LEXFLAGS_COMMENTS_KEEP; } else if ch == 'C' {
shsplit |= LEXFLAGS_COMMENTS_STRIP; } else if ch == 'n' {
shsplit |= LEXFLAGS_NEWLINE; } else {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
idx += 1; }
if idx < body_chars.len() && body_chars[idx] == del {
found_close = true;
idx += 1; }
if !found_close {
zerr("bad substitution"); errflag_set_error();
return (String::new(), new_pos, vec![]);
}
continue; } 'g' => {
idx += 1; if getkeys < 0 {
getkeys = 0; } if idx < body_chars.len() {
let del = body_chars[idx]; idx += 1; while idx < body_chars.len() && body_chars[idx] != del
{
match body_chars[idx] {
'e' => getkeys |= GETKEY_EMACS as i32, 'o' => getkeys |= GETKEY_OCTAL_ESC as i32, 'c' => getkeys |= GETKEY_CTRL as i32, _ => {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
} } idx += 1; } if idx < body_chars.len() {
idx += 1;
}
} continue; } '~' => {
tok_arg = !tok_arg;
} 'm' => {
multi_width += 1;
} 'p' => {
escapes = true;
} '%' => {
presc += 1;
} 'f' => {
spsep = Some("\n".to_string());
} 'F' => {
sep = Some("\n".to_string());
} '0' => {
spsep = Some("\u{0}".to_string());
} 's' | 'j' => {
let is_split = fc == 's'; idx += 1; if idx >= body_chars.len() || body_chars[idx] == ')' {
zerr("bad substitution"); errflag_set_error();
return (String::new(), new_pos, vec![]);
}
let del = body_chars[idx]; let close_del = match del {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
other => other,
};
idx += 1; let s_start = idx;
while idx < body_chars.len() && body_chars[idx] != close_del {
idx += 1;
}
if idx >= body_chars.len() {
zerr("bad substitution"); errflag_set_error();
return (String::new(), new_pos, vec![]);
}
let arg: String = body_chars[s_start..idx].iter().collect(); let arg = untok_and_escape(&arg, escapes, tok_arg); if is_split {
spsep = Some(arg);
} else {
sep = Some(arg);
} if idx < body_chars.len() {
idx += 1;
} continue; }
_ => {
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
}
idx += 1;
}
}
let mut force_split = false;
let mut suppress_split = false;
let mut length_op = false;
let mut chkset = false;
loop {
let c = match body_chars.get(idx).copied() {
Some(ch) => ch,
None => break,
};
if c == '^' || c == Hat {
let nxt = body_chars.get(idx + 1).copied();
if matches!(nxt, Some('^') | Some(Hat)) {
plan9 = false;
idx += 2;
} else {
plan9 = true;
idx += 1;
}
continue;
}
if c == '=' || c == Equals {
let nxt = body_chars.get(idx + 1).copied();
if matches!(nxt, Some('=') | Some(Equals)) {
suppress_split = true;
idx += 2;
} else {
force_split = true;
idx += 1;
}
continue;
}
if c == '#' || c == Pound {
let next = body_chars.get(idx + 1).copied();
let after_next = body_chars.get(idx + 2).copied();
let next_is_name_start = match next {
Some(ch) if ch.is_ascii_alphanumeric() => true,
Some(ch) if matches!(ch, '_' | '@' | '*' | '?' | '!' | '$' | '-' | '0') => true,
Some(':') if matches!(after_next, Some('-') | Some('\u{9b}')) => true,
Some(ch) if ch == STRING || ch == Qstring || ch == Stringg => {
matches!(
body_chars.get(idx + 2).copied(),
Some(b) if b == Inbrace || b == '{' || b == Inpar || b == '('
) || after_next.is_none()
}
Some(ch) if (ch == '#' || ch == Pound) && after_next.is_none() => true,
_ => false,
};
if next_is_name_start {
length_op = true;
idx += 1;
continue;
}
}
if c == '~' {
if body_chars.get(idx + 1).copied() == Some('~') {
if !qt {
opt_state_set("globsubst", false);
}
idx += 2;
} else {
if !qt {
opt_state_set("globsubst", true);
}
idx += 1;
}
continue;
}
if c == '+' {
let nxt = body_chars.get(idx + 1).copied().unwrap_or('\0'); let ok = nxt.is_ascii_alphanumeric()
|| nxt == '_'
|| matches!(nxt, '@' | '*' | '#' | '?')
|| (aspar
&& (nxt == STRING || nxt == Qstring)
&& matches!(
body_chars.get(idx + 2).copied(),
Some(b) if b == Inbrace || b == '{' || b == Inpar || b == '('
));
if ok {
chkset = true;
idx += 1;
continue;
}
zerr("bad substitution");
errflag_set_error();
return (String::new(), new_pos, vec![]);
}
if matches!(c, Snull | Dnull) {
idx += 1;
continue;
}
break;
}
sub_flags_set(sub_flags_bits); let post_flags_start = idx;
let mut peeled_quotes = false; if idx + 1 < body_chars.len() && body_chars[idx] == '"' && body_chars[idx + 1] == '$'
{
let mut p = idx + 1; let mut paren_depth = 0_i32; let mut brace_depth = 0_i32; while p < body_chars.len() {
let ch = body_chars[p]; match ch {
'(' => paren_depth += 1, ')' => paren_depth -= 1, '{' => brace_depth += 1, '}' => brace_depth -= 1, '"' if paren_depth == 0 && brace_depth == 0 => {
idx += 1; peeled_quotes = true; let _ = p; break; } _ => {} } p += 1; } } let next_after_dollar = body_chars.get(idx + 1).copied();
let is_bare_special_dollar = matches!(
next_after_dollar,
Some('}') | Some(')') | Some(Outbrace) | Some(Outpar) | None
);
let mut subexp_value: Option<String> = if idx < body_chars.len()
&& (body_chars[idx] == '$' || body_chars[idx] == Qstring || body_chars[idx] == Stringg)
&& !is_bare_special_dollar
{
let start = idx;
let mut p = idx + 1;
if p < body_chars.len() {
let nx = body_chars[p];
let (open, close): (char, char) = match nx {
'{' => ('{', '}'),
'(' => ('(', ')'),
Inbrace => (Inbrace, Outbrace),
Inpar => (Inpar, Outpar),
_ => ('\0', '\0'),
};
if open != '\0' {
let mut depth = 0_i32;
while p < body_chars.len() {
let ch = body_chars[p];
if ch == open {
depth += 1;
} else if ch == close {
depth -= 1;
if depth == 0 {
p += 1;
break;
}
}
p += 1;
}
} else {
p += 1;
while p < body_chars.len()
&& (body_chars[p].is_ascii_alphanumeric() || body_chars[p] == '_')
{
p += 1;
}
}
}
let inner: String = body_chars[start..p].iter().collect(); let expanded = if (nojoin == 2) {
let (joined, arr_parts, isarr, _) = multsub(&inner, PREFORK_SPLIT);
if isarr && !arr_parts.is_empty() {
static SEQ: AtomicUsize = AtomicUsize::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let temp = format!("__subexp_arr_{}", n);
arrays_insert(temp.clone(), arr_parts);
subexp_array_temp = Some(temp.clone());
temp
} else {
joined
}
} else {
singsub(&inner) };
idx = p; if peeled_quotes && idx < body_chars.len() && body_chars[idx] == '"' {
idx += 1; } Some(expanded)
} else {
None
};
let name_start = idx;
if idx < body_chars.len() && body_chars[idx] == '\u{01}' {
zerr("bad substitution");
errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::Relaxed);
return (String::new(), idx + 1, Vec::new());
}
if subexp_value.is_none() {
while idx < body_chars.len() {
let bc = body_chars[idx];
let allowed = if idx == name_start {
bc.is_ascii_alphanumeric()
|| bc == '_'
|| bc == '@'
|| bc == '*' || bc == '\u{87}'
|| bc == '#' || bc == Pound
|| bc == '?' || bc == '\u{86}'
|| bc == '!' || bc == '\u{96}'
|| bc == '0'
|| bc == '-' || bc == '\u{9b}'
|| bc == '$' || bc == Stringg
} else {
bc.is_ascii_alphanumeric() || bc == '_'
};
if allowed {
idx += 1;
let first = body_chars[name_start];
if idx == name_start + 1
&& (matches!(first, '@' | '*' | '#' | '?' | '0' | '!' | '-' | '$')
|| first == Pound
|| first == '\u{87}'
|| first == '\u{86}'
|| first == '\u{96}'
|| first == '\u{9b}'
|| first == Stringg)
{
break;
}
} else {
break;
}
}
}
let mut var_name: String = {
let raw: String = body_chars[name_start..idx].iter().collect();
if raw.chars().any(|c| {
let cu = c as u32;
(0x84..=0xa1).contains(&cu)
}) {
crate::lex::untokenize(&raw)
} else {
raw
}
};
if (var_name == "!" || var_name == "\u{96}") && idx < body_chars.len() {
let nx = body_chars[idx];
if nx.is_ascii_alphanumeric()
|| nx == '_'
|| nx == '@'
|| nx == '*'
|| nx == '\u{87}'
|| nx == '!'
|| nx == '\u{96}'
{
zerr("bad substitution");
errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::Relaxed);
return (String::new(), new_pos, Vec::new());
}
}
if let Some(ref temp) = subexp_array_temp {
var_name = temp.clone();
subexp_value = None;
}
let mut subscript: Option<String> = None; if idx < body_chars.len() && (body_chars[idx] == '[' || body_chars[idx] == Inbrack) {
idx += 1; let sub_start = idx;
let mut depth = 1_i32;
while idx < body_chars.len() && depth > 0 {
let bc = body_chars[idx];
if bc == '[' || bc == Inbrack {
depth += 1;
}
else if bc == ']' || bc == Outbrack {
depth -= 1;
if depth == 0 {
break;
}
}
idx += 1;
}
if idx > sub_start {
let raw_sub: String = body_chars[sub_start..idx].iter().collect();
subscript = Some(singsub(&raw_sub)); }
if idx < body_chars.len() {
idx += 1;
} }
let mut second_subscript: Option<String> = None;
if idx < body_chars.len() && (body_chars[idx] == '[' || body_chars[idx] == Inbrack) {
idx += 1;
let sub2_start = idx;
let mut depth = 1_i32;
while idx < body_chars.len() && depth > 0 {
let bc = body_chars[idx];
if bc == '[' || bc == Inbrack {
depth += 1;
} else if bc == ']' || bc == Outbrack {
depth -= 1;
if depth == 0 {
break;
}
}
idx += 1;
}
if idx > sub2_start {
let raw2: String = body_chars[sub2_start..idx].iter().collect();
second_subscript = Some(singsub(&raw2));
}
if idx < body_chars.len() {
idx += 1;
} }
let rest: String = {
let raw: String = body_chars[idx..].iter().collect();
if raw.chars().any(|c| {
let cu = c as u32;
(0x84..=0xa1).contains(&cu)
}) {
crate::lex::untokenize(&raw)
} else {
raw
}
};
if aspar {
if let Some(sv) = subexp_value.clone() {
var_name = sv.trim().to_string(); subexp_value = None; } else {
let target = vars_get(&var_name) .or_else(|| arrays_get(&var_name).map(|a| a.join(" "))) .unwrap_or_default(); var_name = target; } }
let used_subexp = subexp_value.is_some();
let raw_value: String = if let Some(sv) = subexp_value {
sv } else if let Some(sub) = subscript.as_deref() {
if let Some(map) = assoc_get(&var_name) {
if let Some((flags, pat)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let flags = rest[..close].to_string();
let pat = rest[close + 1..].to_string();
if flags
.chars()
.all(|c| matches!(c, 'I' | 'R' | 'i' | 'r' | 'k' | 'K' | 'n' | 'e' | 'b'))
{
Some((flags, pat))
} else {
None
}
})(sub)
{
let by_key = flags.contains('I') || flags.contains('i');
let return_all = flags.contains('I') || flags.contains('R');
let exact = flags.contains('e'); let mut out: Vec<String> = Vec::new();
for (k, v) in map.iter() {
let hay = if by_key { k.as_str() } else { v.as_str() };
let matched = if exact {
hay == pat.as_str()
} else {
patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, hay))
};
if matched {
out.push(if by_key { k.clone() } else { v.clone() });
if !return_all {
break;
}
}
}
out.join(" ")
} else {
map.get(sub).cloned().unwrap_or_default()
}
} else if let Some(arr) = arrays_get(&var_name) {
if sub == "*" || sub == "@" {
arr.join(" ")
} else if let Some((flags, pat)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let flags = rest[..close].to_string();
let pat = rest[close + 1..].to_string();
if flags
.chars()
.all(|c| matches!(c, 'I' | 'R' | 'i' | 'r' | 'n' | 'e'))
{
Some((flags, pat))
} else {
None
}
})(sub)
{
let return_index = flags.contains('I') || flags.contains('i'); let down = flags.contains('I') || flags.contains('R'); let exact = flags.contains('e'); let mut found_idx: Option<usize> = None; let iter: Box<dyn Iterator<Item = (usize, &String)>> = if down {
Box::new(arr.iter().enumerate().rev())
} else {
Box::new(arr.iter().enumerate())
};
for (idx, elem) in iter {
let matched = if exact {
elem == &pat
} else {
patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, elem))
};
if matched {
found_idx = Some(idx);
break;
}
}
match found_idx {
Some(idx) if return_index => (idx + 1).to_string(),
Some(idx) => arr[idx].clone(),
None if return_index && !down => {
(arr.len() + 1).to_string()
}
None if return_index && down => {
"0".to_string()
}
None => String::new(),
}
} else if let Some((flag_body, num_str)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let f = rest[..close].to_string();
let n = rest[close + 1..].to_string();
if !f.chars().all(|c| matches!(c, 'w' | 'W' | 'p')) {
return None;
}
Some((f, n))
})(sub)
{
let _ = flag_body;
if let Ok(idx_n) = num_str.parse::<i64>() {
let len = arr.len() as i64;
let i = if idx_n == 0 {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
0
} else {
-1
}
} else if idx_n < 0 {
len + idx_n
} else {
idx_n - 1
};
if i >= 0 && (i as usize) < arr.len() {
arr[i as usize].clone()
} else {
String::new()
}
} else {
String::new()
}
} else if let Some(idx_n) = sub
.parse::<i64>()
.ok()
.or_else(|| {
let s = sub.trim();
if s.starts_with('(') && s.ends_with(')') && s.len() >= 2 {
s[1..s.len() - 1].trim().parse::<i64>().ok()
} else {
None
}
})
.or_else(|| {
if sub.contains(',') {
None
} else {
crate::ported::math::mathevali(sub).ok()
}
})
{
let len = arr.len() as i64;
let ksh_arrays = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS);
let i = if ksh_arrays {
if idx_n < 0 {
len + idx_n
} else {
idx_n
}
} else if idx_n == 0 {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
0 } else {
-1 }
} else if idx_n < 0 {
len + idx_n
} else {
idx_n - 1
};
if i >= 0 && (i as usize) < arr.len() {
arr[i as usize].clone()
} else {
String::new()
}
} else if let Some((start_s, end_s)) = sub.split_once(',') {
let arr_clone = arr.clone();
let len = arr_clone.len() as i64;
let start_str = start_s.to_string();
let end_str = end_s.to_string();
let start: i64 = singsub(&start_str).parse().unwrap_or(1);
let end: i64 = singsub(&end_str).parse().unwrap_or(len);
let start_oob_neg = start < 0 && (len + start) < 0;
let s_raw: i64 = if start < 0 { len + start } else { start - 1 };
let e_raw: i64 = if end < 0 { len + end + 1 } else { end.min(len) };
if start_oob_neg {
String::new()
} else {
let s = s_raw.max(0) as usize;
let e = e_raw.max(0) as usize;
if s < arr_clone.len() && s < e {
arr_clone[s..e.min(arr_clone.len())].join(" ")
} else {
String::new()
}
}
} else {
String::new()
}
} else if let Some(magic_val) = {
let is_splice = sub == "@" || sub == "*";
if is_splice {
if let Some(values) = crate::vm_helper::partab_array_get(&var_name) {
Some(values.join(" "))
} else if let Some(keys) = crate::vm_helper::partab_scan_keys(&var_name) {
let vals: Vec<String> = keys
.iter()
.map(|k| crate::vm_helper::partab_get(&var_name, k).unwrap_or_default())
.collect();
Some(vals.join(" "))
} else {
None
}
} else if let Some((flags, pat)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let flags = rest[..close].to_string();
let pat = rest[close + 1..].to_string();
if flags
.chars()
.all(|c| matches!(c, 'I' | 'R' | 'i' | 'r' | 'k' | 'K' | 'n' | 'e' | 'b'))
{
Some((flags, pat))
} else {
None
}
})(sub)
{
if let Some(keys) = crate::vm_helper::partab_scan_keys(&var_name) {
let by_key = flags.contains('I') || flags.contains('i');
let return_all = flags.contains('I') || flags.contains('R');
let exact = flags.contains('e'); let mut out: Vec<String> = Vec::new();
for k in &keys {
let hay = if by_key {
k.clone()
} else {
crate::vm_helper::partab_get(&var_name, k).unwrap_or_default()
};
let matched = if exact {
hay == pat
} else {
patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &hay))
};
if matched {
out.push(if by_key {
k.clone()
} else {
crate::vm_helper::partab_get(&var_name, k).unwrap_or_default()
});
if !return_all {
break;
}
}
}
Some(out.join(" "))
} else {
None
}
} else {
crate::vm_helper::partab_get(&var_name, sub)
}
} {
magic_val
} else {
let scalar = vars_get(&var_name).unwrap_or_default();
let s_chars: Vec<char> = scalar.chars().collect();
if let Some((flags, pat)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let f = rest[..close].to_string();
let p = rest[close + 1..].to_string();
if f.chars()
.all(|c| matches!(c, 'I' | 'R' | 'i' | 'r' | 'n' | 'e' | 'b'))
{
Some((f, p))
} else {
None
}
})(sub)
{
let return_index = flags.contains('I') || flags.contains('i');
let want_last = flags.contains('I') || flags.contains('R');
let exact = flags.contains('e'); let n = s_chars.len();
let mut found: Option<(usize, usize)> = None;
'outer: for start in 0..=n {
let lengths: Box<dyn Iterator<Item = usize>> = if want_last {
Box::new((1..=(n - start)).rev())
} else {
Box::new(1..=(n - start))
};
for len in lengths {
let cand: String = s_chars[start..start + len].iter().collect();
let matched = if exact {
cand == pat
} else {
patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
};
if matched {
found = Some((start, start + len));
if !want_last {
break 'outer;
}
break;
}
}
}
if want_last {
for start in (0..=n).rev() {
for len in 1..=(n - start) {
let cand: String = s_chars[start..start + len].iter().collect();
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
{
found = Some((start, start + len));
break;
}
}
if found.is_some() && found.unwrap().0 >= start {
break;
}
}
}
match (found, return_index) {
(Some((s, _)), true) => (s + 1).to_string(),
(Some((s, e)), false) => s_chars[s..e].iter().collect(),
(None, true) => {
if flags.contains('i') {
(n + 1).to_string()
} else {
"0".to_string()
}
}
(None, false) => String::new(),
}
} else if let Some(idx_n) = sub
.parse::<i64>()
.ok()
.or_else(|| {
let s = sub.trim();
if s.starts_with('(') && s.ends_with(')') && s.len() >= 2 {
s[1..s.len() - 1].trim().parse::<i64>().ok()
} else {
None
}
})
.or_else(|| {
if sub.contains(',') {
None
} else {
crate::ported::math::mathevali(sub).ok()
}
})
{
let len = s_chars.len() as i64;
let i = if idx_n == 0 {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
0 } else {
-1 }
} else if idx_n < 0 {
len + idx_n
} else {
idx_n - 1
};
if i >= 0 && (i as usize) < s_chars.len() {
s_chars[i as usize].to_string()
} else {
String::new()
}
} else if let Some((lo, hi)) = sub.split_once(',') {
let lo: i64 = lo.trim().parse().unwrap_or(1);
let hi: i64 = hi.trim().parse().unwrap_or(s_chars.len() as i64);
let chars_arr: Vec<String> = s_chars.iter().map(|c| c.to_string()).collect();
getarrvalue(&chars_arr, lo, hi).concat()
} else {
String::new()
}
}
} else {
let is_special_name = (var_name.len() == 1
&& matches!(
var_name.chars().next().unwrap_or('\0'),
'#' | '?' | '!' | '$' | '*' | '@' | '-'
))
|| (!var_name.is_empty() && var_name.chars().all(|c| c.is_ascii_digit()));
exec_getsparam(&var_name)
.or_else(|| {
assoc_get(&var_name).map(|m| m.values().cloned().collect::<Vec<_>>().join(" "))
})
.or_else(|| {
if is_special_name {
lookup_special_var(&var_name)
} else {
None
}
})
.unwrap_or_default()
};
let raw_value: String = if let Some(s2) = second_subscript.as_deref() {
let n = raw_value.chars().count() as i64;
let resolve = |k: i64| -> usize {
let k = if k < 0 { n + k + 1 } else { k };
if k < 1 {
0
} else if k > n {
n as usize
} else {
(k - 1) as usize
}
};
if let Some((lo, hi)) = s2.split_once(',') {
let lo: i64 = lo
.trim()
.parse()
.ok()
.or_else(|| crate::ported::math::mathevali(lo.trim()).ok())
.unwrap_or(1);
let hi: i64 = hi
.trim()
.parse()
.ok()
.or_else(|| crate::ported::math::mathevali(hi.trim()).ok())
.unwrap_or(0);
let l = resolve(lo);
let h = if hi == 0 {
n as usize
} else {
let k = if hi < 0 { n + hi + 1 } else { hi };
if k < 1 {
0
} else if k > n {
n as usize
} else {
k as usize
}
};
if l >= h {
String::new()
} else {
raw_value.chars().skip(l).take(h - l).collect()
}
} else {
let k: i64 = s2
.trim()
.parse()
.ok()
.or_else(|| crate::ported::math::mathevali(s2.trim()).ok())
.unwrap_or(1);
let i = resolve(k);
raw_value
.chars()
.nth(i)
.map(|c| c.to_string())
.unwrap_or_default()
}
} else {
raw_value
};
let is_set = if let Some(sub) = subscript.as_deref() {
let is_at_or_star = matches!(sub, "@" | "*");
let is_slice = sub.contains(',');
used_subexp
|| assoc_get(&var_name)
.map(|m| m.contains_key(sub))
.unwrap_or(false)
|| arrays_get(&var_name)
.as_ref()
.map(|a| {
if is_at_or_star || is_slice {
!a.is_empty()
} else {
sub.parse::<i64>().ok().is_some_and(|i| {
let len = a.len() as i64;
let real = if i < 0 { len + i } else { i - 1 };
real >= 0 && (real as usize) < a.len()
})
}
})
.unwrap_or(false)
|| ((is_at_or_star || is_slice) && vars_contains(&var_name))
|| crate::vm_helper::partab_get(&var_name, sub).is_some_and(|v| !v.is_empty())
} else {
let positional_set = !var_name.is_empty()
&& var_name.chars().all(|c| c.is_ascii_digit())
&& var_name.parse::<usize>().ok().is_some_and(|n| {
if n == 0 {
vars_get("0").is_some()
} else {
arrays_get("@").map_or(false, |a| n <= a.len())
}
});
used_subexp
|| vars_contains(&var_name)
|| arrays_contains(&var_name)
|| assoc_contains(&var_name)
|| positional_set
};
if chkset {
let set_str = if subscript.is_some() {
if !raw_value.is_empty() {
"1"
} else {
"0"
}
} else if is_set {
"1"
} else {
"0"
};
let prefix: String = chars[..start_pos].iter().collect();
let suffix: String = if new_pos < chars.len() {
chars[new_pos..].iter().collect()
} else {
String::new()
};
let full = format!("{}{}{}", prefix, set_str, suffix);
let new_pos_in_full = prefix.chars().count() + set_str.chars().count();
return (full.clone(), new_pos_in_full, vec![full]); }
if length_op {
let _ = post_flags_start;
let getlen = 1 + whichlen; let raw_value_for_len = {
let r = rest.as_str();
if let Some(default) = r.strip_prefix(":-") {
if raw_value.is_empty() {
singsub(default)
} else {
raw_value.clone()
}
} else if let Some(default) = r.strip_prefix('-') {
if !vars_contains(&var_name)
&& !arrays_contains(&var_name)
&& !assoc_contains(&var_name)
{
singsub(default)
} else {
raw_value.clone()
}
} else if let Some(alt) = r.strip_prefix(":+") {
if !raw_value.is_empty() {
singsub(alt)
} else {
String::new()
}
} else if let Some(alt) = r.strip_prefix('+') {
if vars_contains(&var_name)
|| arrays_contains(&var_name)
|| assoc_contains(&var_name)
{
singsub(alt)
} else {
String::new()
}
} else {
raw_value.clone()
}
};
let magic_keys: Option<Vec<String>> =
if !arrays_contains(&var_name) && !assoc_contains(&var_name) {
crate::vm_helper::partab_scan_keys(&var_name)
.or_else(|| crate::vm_helper::partab_array_get(&var_name))
} else {
None
};
let single_slot_subscript = subscript
.as_deref()
.map_or(false, |s| s != "@" && s != "*" && !s.contains(','));
let is_array_source =
(arrays_contains(&var_name) || assoc_contains(&var_name) || magic_keys.is_some())
&& !single_slot_subscript;
let n: usize = if is_array_source {
if getlen == 1 {
if let Some(arr) = arrays_get(&var_name) {
arr.len() } else if let Some(map) = assoc_get(&var_name) {
map.len() } else if let Some(ref keys) = magic_keys {
keys.len()
} else {
0
}
} else if getlen == 2 {
let arr: Vec<String> = if let Some(a) = arrays_get(&var_name) {
a
} else if let Some(m) = assoc_get(&var_name) {
m.values().cloned().collect()
} else {
Vec::new()
};
if arr.is_empty() {
0
} else {
let sl = sep.as_deref().map(|s| s.chars().count()).unwrap_or(1); let mut len: i64 = -(sl as i64); for elem in &arr {
len += (sl as i64) + (elem.chars().count() as i64); }
len.max(0) as usize
}
} else {
let multi = if getlen > 3 { 1 } else { 0 }; let arr: Vec<String> = if let Some(a) = arrays_get(&var_name) {
a
} else if let Some(m) = assoc_get(&var_name) {
m.values().cloned().collect()
} else {
Vec::new()
};
let mut total: i32 = 0;
for elem in &arr {
total += crate::ported::utils::wordcount(elem, spsep.as_deref(), multi);
}
total.max(0) as usize
}
} else {
if getlen < 3 {
raw_value_for_len.chars().count()
} else {
let multi = if getlen > 3 { 1 } else { 0 };
crate::ported::utils::wordcount(&raw_value_for_len, spsep.as_deref(), multi)
.max(0) as usize
}
};
let n_str = n.to_string();
let prefix: String = chars[..start_pos].iter().collect();
let suffix: String = if new_pos < chars.len() {
chars[new_pos..].iter().collect()
} else {
String::new()
};
let full = format!("{}{}{}", prefix, n_str, suffix);
let new_pos_in_full = prefix.chars().count() + n_str.chars().count();
return (full.clone(), new_pos_in_full, vec![full]);
}
let mut value: String; let mut magic_assoc_array: Option<Vec<String>> = None;
if (hkeys & SCANPM_WANTKEYS) != 0 && (hvals & SCANPM_WANTVALS) != 0 {
magic_assoc_array = assoc_get(&var_name)
.map(|m| {
let mut out: Vec<String> = Vec::with_capacity(m.len() * 2);
for (k, v) in m {
out.push(k.clone());
out.push(v.clone());
}
out
})
.or_else(|| match var_name.as_str() {
"aliases" => aliastab_lock().read().ok().map(|t| {
let mut entries: Vec<(String, String)> =
t.iter().map(|(k, v)| (k.clone(), v.text.clone())).collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
entries.into_iter().flat_map(|(k, v)| [k, v]).collect()
}),
_ => crate::vm_helper::partab_scan_keys(&var_name).map(|mut keys| {
keys.sort();
keys.into_iter()
.flat_map(|k| {
let v =
crate::vm_helper::partab_get(&var_name, &k).unwrap_or_default();
[k, v]
})
.collect()
}),
});
value = magic_assoc_array
.as_ref()
.map(|v| v.join(" "))
.unwrap_or_default(); } else if (hkeys & SCANPM_WANTKEYS) != 0 {
magic_assoc_array = assoc_get(&var_name)
.map(|m| m.keys().cloned().collect::<Vec<String>>())
.or_else(|| match var_name.as_str() {
"aliases" => aliastab_lock().read().ok().map(|t| {
let mut names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
names.sort();
names
}),
"functions" | "dis_functions" => shfunctab_lock().read().ok().map(|t| {
let mut names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
names.sort();
names
}),
"commands" => cmdnamtab_lock().read().ok().map(|t| {
let mut names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
names.sort();
names
}),
_ => crate::vm_helper::partab_scan_keys(&var_name).map(|mut keys| {
keys.sort();
keys
}),
})
.or_else(|| crate::ported::exec_hooks::array(&var_name));
value = assoc_get(&var_name) .map(|m| m.keys().cloned().collect::<Vec<_>>().join(" ")) .or_else(|| {
crate::ported::exec_hooks::array(&var_name).map(|a| a.join(" "))
})
.or_else(|| {
match var_name.as_str() {
"aliases" => aliastab_lock().read().ok().map(|t| {
let mut names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
names.sort();
names.join(" ")
}),
"functions" | "dis_functions" => shfunctab_lock().read().ok().map(|t| {
let mut names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
names.sort();
names.join(" ")
}),
"commands" => cmdnamtab_lock().read().ok().map(|t| {
let mut names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
names.sort();
names.join(" ")
}),
_ => crate::vm_helper::partab_scan_keys(&var_name).map(|mut keys| {
keys.sort();
keys.join(" ")
}),
} }) .unwrap_or_default();
} else if (hvals & SCANPM_WANTVALS) != 0 {
magic_assoc_array = assoc_get(&var_name)
.map(|m| m.values().cloned().collect::<Vec<String>>())
.or_else(|| {
crate::vm_helper::partab_array_get(&var_name).or_else(|| {
crate::vm_helper::partab_scan_keys(&var_name).map(|mut keys| {
keys.sort();
keys.into_iter()
.map(|k| {
crate::vm_helper::partab_get(&var_name, &k).unwrap_or_default()
})
.collect()
})
})
})
.or_else(|| crate::ported::exec_hooks::array(&var_name));
value = magic_assoc_array
.as_ref()
.map(|v| v.join(" "))
.unwrap_or_default();
isarr = 1;
} else if (nojoin == 2) {
value = arrays_get(&var_name)
.as_ref()
.map(|a| a.join(" "))
.unwrap_or_else(|| raw_value.clone());
if arrays_contains(&var_name) || assoc_contains(&var_name) {
isarr = 1;
}
} else {
value = raw_value.clone();
let is_at_subscript = matches!(subscript.as_deref(), Some("@") | Some("*"));
if (arrays_contains(&var_name) || assoc_contains(&var_name))
&& (subscript.is_none() || is_at_subscript)
{
isarr = if is_at_subscript { -1 } else { 1 };
}
}
if !plan9 && (nojoin == 2) {
if let Some(ref a) = arrays_get(&var_name) {
if a.first().map_or(true, |s| s.is_empty()) {
value = String::new();
}
}
}
if isarr != 0 {
if nojoin != 0 {
isarr = -1; }
if qt && isarr > 0 && spsep.is_none() {
isarr = 0; }
}
let mut split_parts: Option<Vec<String>> = None; if let Some(ref keys) = magic_assoc_array {
if !keys.is_empty() {
split_parts = Some(keys.clone());
isarr = 1;
}
}
if !rest.is_empty() {
let r = rest.as_str();
if let Some(pat) = r.strip_prefix(":#") {
let p = singsub(pat); let cur_sub_flags = sub_flags_get(); let invert = (cur_sub_flags & 0x0008) != 0; sub_flags_set(0); let is_array_subscript = matches!(subscript.as_deref(), Some("@") | Some("*"))
|| subscript.as_deref().map_or(false, |s| s.contains(','));
let has_subscript = subscript.is_some() && !is_array_subscript;
let per_element_array = !has_subscript && (!qt || is_array_subscript);
if let Some(arr) = arrays_get(&var_name).filter(|_| per_element_array) {
let kept: Vec<String> = arr
.into_iter() .filter(|elem| {
let m = patcompile(&p, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, elem)); if invert {
m
} else {
!m
} }) .collect();
value = kept.join(" "); split_parts = Some(kept); } else {
let m = patcompile(&p, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &raw_value)); value = if invert {
if m {
raw_value.clone()
} else {
String::new()
} } else {
if m {
String::new()
} else {
raw_value.clone()
} }; } } else if let Some(default) = r.strip_prefix(":-") {
if !is_set || raw_value.is_empty() {
value = singsub(default);
if isarr != 0 && split_parts.is_none() && !value.is_empty() {
split_parts = Some(vec![value.clone()]);
}
}
} else if let Some(default) = r.strip_prefix('-') {
if !is_set {
value = singsub(default);
}
} else if let Some(default) = r.strip_prefix("::=") {
value = singsub(default);
if arrasg == 1 {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
exec_assignaparam(&var_name, parts);
} else if arrasg == 2 {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
exec_sethparam(&var_name, parts);
} else {
let __s = match subscript.as_deref() {
Some(k) => format!("{}[{}]", var_name, k),
None => var_name.clone(),
};
assignsparam(&__s, &value, 0);
exec_sync_state_from_paramtab();
}
} else if let Some(default) = r.strip_prefix(":=") {
if !is_set || raw_value.is_empty() {
value = singsub(default);
if arrasg == 1 {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
exec_assignaparam(&var_name, parts);
} else if arrasg == 2 {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
exec_sethparam(&var_name, parts);
} else {
let __s = match subscript.as_deref() {
Some(k) => format!("{}[{}]", var_name, k),
None => var_name.clone(),
};
assignsparam(&__s, &value, 0);
exec_sync_state_from_paramtab();
}
}
} else if let Some(default) = r.strip_prefix('=') {
if !is_set {
value = singsub(default);
if arrasg == 1 {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
exec_assignaparam(&var_name, parts);
} else if arrasg == 2 {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
exec_sethparam(&var_name, parts);
} else {
let __s = match subscript.as_deref() {
Some(k) => format!("{}[{}]", var_name, k),
None => var_name.clone(),
};
assignsparam(&__s, &value, 0);
exec_sync_state_from_paramtab();
}
}
} else if let Some(alt) = r.strip_prefix(":+") {
if is_set && !raw_value.is_empty() {
value = singsub(alt);
} else {
value = String::new();
}
if isarr != 0 && split_parts.is_none() && !value.is_empty() {
split_parts = Some(vec![value.clone()]);
}
} else if let Some(alt) = r.strip_prefix('+') {
if is_set {
value = singsub(alt);
} else {
value = String::new();
}
if isarr != 0 && split_parts.is_none() && !value.is_empty() {
split_parts = Some(vec![value.clone()]);
}
} else if let Some(msg) = r.strip_prefix(":?") {
if !is_set || raw_value.is_empty() {
let m = if msg.is_empty() {
"parameter not set".to_string()
} else {
singsub(msg) }; zerr(&format!("{}: {}", var_name, m));
errflag_set_error();
}
} else if let Some(msg) = r.strip_prefix('?') {
if !is_set {
let m = if msg.is_empty() {
"parameter not set".to_string() } else {
singsub(msg) }; zerr(&format!("{}: {}", var_name, m));
errflag_set_error();
}
} else if let Some(rep) = r.strip_prefix(":/") {
let parts: Vec<&str> = rep.splitn(2, '/').collect();
let pat = singsub(parts[0]);
let repl = parts.get(1).map(|s| singsub(s)).unwrap_or_default();
if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr
.into_iter()
.map(|elem| {
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &elem))
{
repl.clone()
} else {
elem
}
})
.collect();
value = new_arr.join(" "); split_parts = Some(new_arr); } else if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &raw_value))
{
value = repl; } else {
value = raw_value.clone(); }
} else if let Some(rep) = r.strip_prefix("//") {
let split_unescaped = |s: &str| -> (String, String) {
let cv: Vec<char> = s.chars().collect();
let mut pat_buf = String::new();
let mut i = 0;
while i < cv.len() {
let c = cv[i];
if (c == '\x00' || c == '\u{9f}') && i + 1 < cv.len() {
pat_buf.push('\\');
pat_buf.push(cv[i + 1]);
i += 2;
continue;
}
if c == '\\' && i + 1 < cv.len() && cv[i + 1] == '/' {
pat_buf.push(cv[i + 1]);
i += 2;
continue;
}
if c == '/' {
let rest: String = cv[i + 1..].iter().collect();
return (pat_buf, rest);
}
pat_buf.push(c);
i += 1;
}
(pat_buf, String::new())
};
let (raw_pat, raw_repl) = split_unescaped(rep);
let (pat_anchor, pat_after_anchor) = if let Some(rest) = raw_pat.strip_prefix('#') {
('#', rest.to_string())
} else if let Some(rest) = raw_pat.strip_prefix('%') {
('%', rest.to_string())
} else {
('\0', raw_pat.clone())
};
let pat = singsub(&pat_after_anchor);
let repl = {
let saved_skip = SKIP_FILESUB.with(|c| c.get());
SKIP_FILESUB.with(|c| c.set(true));
let s = untokenize(&singsub(&raw_repl));
SKIP_FILESUB.with(|c| c.set(saved_skip));
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c == '\\' {
if let Some(&nx) = it.peek() {
if nx == '\\' {
out.push('\\');
it.next();
continue;
}
out.push(nx);
it.next();
continue;
}
}
out.push(c);
}
out
};
let pat_has_m =
pat_after_anchor.contains("(#m)") || pat_after_anchor.contains("(#b)");
let eval_repl_for_match = |span_text: &str, span_start_byte: usize| -> String {
if !pat_has_m {
return repl.clone();
}
crate::ported::params::setsparam("MATCH", span_text);
let base = if isset(crate::ported::zsh_h::KSHARRAYS) {
0i64
} else {
1
};
crate::ported::params::setiparam("MBEGIN", span_start_byte as i64 + base);
crate::ported::params::setiparam(
"MEND",
(span_start_byte as i64 + span_text.len() as i64 + base).saturating_sub(1),
);
let saved_skip = SKIP_FILESUB.with(|c| c.get());
SKIP_FILESUB.with(|c| c.set(true));
let s = untokenize(&singsub(&raw_repl));
SKIP_FILESUB.with(|c| c.set(saved_skip));
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c == '\\' {
if let Some(&nx) = it.peek() {
if nx == '\\' {
out.push('\\');
it.next();
continue;
}
out.push(nx);
it.next();
continue;
}
}
out.push(c);
}
out
};
let replace_global = |val: &str| -> String {
let cv: Vec<char> = val.chars().collect();
let nn = cv.len();
let mut o = String::with_capacity(val.len());
if pat_anchor == '#' {
let mut matched: Option<usize> = None;
for end in (1..=nn).rev() {
let cand: String = cv[..end].iter().collect();
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
{
matched = Some(end);
break;
}
}
if let Some(e) = matched {
let span: String = cv[..e].iter().collect();
o.push_str(&eval_repl_for_match(&span, 0));
o.push_str(&cv[e..].iter().collect::<String>());
} else {
o.push_str(val);
}
return o;
}
if pat_anchor == '%' {
let mut matched: Option<usize> = None;
for start in 0..=nn {
let cand: String = cv[start..].iter().collect();
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
{
matched = Some(start);
break;
}
}
if let Some(s) = matched {
let span_text: String = cv[s..].iter().collect();
let span_byte = cv[..s].iter().map(|c| c.len_utf8()).sum();
o.push_str(&cv[..s].iter().collect::<String>());
o.push_str(&eval_repl_for_match(&span_text, span_byte));
} else {
o.push_str(val);
}
return o;
}
let mut q = 0_usize;
while q < nn {
let mut m: Option<usize> = None;
for e in (q + 1..=nn).rev() {
let c: String = cv[q..e].iter().collect();
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &c))
{
m = Some(e);
break;
}
}
if let Some(e) = m {
let span_text: String = cv[q..e].iter().collect();
let span_byte = cv[..q].iter().map(|c| c.len_utf8()).sum::<usize>();
o.push_str(&eval_repl_for_match(&span_text, span_byte));
q = if e == q { q + 1 } else { e };
} else {
o.push(cv[q]);
q += 1;
}
}
o
};
let mut handled_array = false;
let has_scalar_subscript = subscript
.as_deref()
.map(|s| {
let t = s.trim();
t != "@" && t != "*" && !t.contains(',')
})
.unwrap_or(false);
let has_subscript = has_scalar_subscript;
if let Some(arr) = arrays_get(&var_name).filter(|_| !has_subscript) {
let new_arr: Vec<String> = arr.iter().map(|e| replace_global(e)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr); handled_array = true;
}
if handled_array {
let _ = handled_array;
} else {
value = replace_global(&raw_value);
} } else if let Some(rep) = r.strip_prefix('/') {
let split_unescaped = |s: &str| -> (String, String) {
let cv: Vec<char> = s.chars().collect();
let mut pat_buf = String::with_capacity(s.len());
let mut i = 0;
while i < cv.len() {
let c = cv[i];
if (c == '\x00' || c == '\u{9f}' || c == '\\') && i + 1 < cv.len() {
if cv[i + 1] == '/' {
pat_buf.push('/');
i += 2;
continue;
}
pat_buf.push(c);
pat_buf.push(cv[i + 1]);
i += 2;
continue;
}
if c == '/' {
let rest: String = cv[i + 1..].iter().collect();
return (pat_buf, rest);
}
pat_buf.push(c);
i += 1;
}
(pat_buf, String::new())
};
let (raw_pat, raw_repl) = split_unescaped(rep);
let pat = singsub(&raw_pat);
if std::env::var("ZSHRS_TRACE_REPL2").is_ok() {
eprintln!(
"[TRACE_REPL2] rep={:?} raw_pat={:?} pat={:?} raw_repl={:?}",
rep, raw_pat, pat, raw_repl
);
}
let repl = {
let saved_skip = SKIP_FILESUB.with(|c| c.get());
SKIP_FILESUB.with(|c| c.set(true));
let s_singsub = singsub(&raw_repl);
SKIP_FILESUB.with(|c| c.set(saved_skip));
let s = untokenize(&s_singsub);
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c == '\\' {
if let Some(&nx) = it.peek() {
if nx == '\\' {
out.push('\\');
it.next();
continue;
}
out.push(nx);
it.next();
continue;
}
}
out.push(c);
}
out
};
let replace_one = |val: &str| -> String {
if let Some(anchor_pat) = pat.strip_prefix('#') {
let cv: Vec<char> = val.chars().collect();
let nn = cv.len();
for end in (0..=nn).rev() {
let cand: String = cv[..end].iter().collect();
if patcompile(anchor_pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
{
return format!("{}{}", repl, cv[end..].iter().collect::<String>());
}
}
val.to_string()
} else if let Some(anchor_pat) = pat.strip_prefix('%') {
let cv: Vec<char> = val.chars().collect();
let nn = cv.len();
for start in 0..=nn {
let cand: String = cv[start..].iter().collect();
if patcompile(anchor_pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
{
return format!(
"{}{}",
cv[..start].iter().collect::<String>(),
repl
);
}
}
val.to_string()
} else {
let cv: Vec<char> = val.chars().collect();
let nn = cv.len();
for start in 0..nn {
for end in (start + 1..=nn).rev() {
let cand: String = cv[start..end].iter().collect();
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &cand))
{
let mut out = String::with_capacity(val.len());
out.extend(cv[..start].iter());
out.push_str(&repl);
out.extend(cv[end..].iter());
return out;
}
}
}
val.to_string()
}
};
let has_subscript_one = subscript
.as_deref()
.map(|s| {
let t = s.trim();
t != "@" && t != "*" && !t.contains(',')
})
.unwrap_or(false);
if let Some(arr) = arrays_get(&var_name).filter(|_| !has_subscript_one) {
let is_at_star_subscript =
matches!(subscript.as_deref(), Some("@") | Some("*"));
let array_shape = is_at_star_subscript || nojoin == 2 || !qt;
if array_shape {
let new_arr: Vec<String> = arr.iter().map(|e| replace_one(e)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr);
} else {
let mut done = false;
let new_arr: Vec<String> = arr
.iter()
.map(|e| {
if done {
e.clone()
} else {
let replaced = replace_one(e);
if replaced != *e {
done = true;
}
replaced
}
})
.collect();
value = new_arr.join(" "); split_parts = Some(new_arr); }
} else {
value = replace_one(&raw_value); }
} else if let Some(pat) = r.strip_prefix("##") {
let p = singsub(pat);
let has_scalar_sub = subscript
.as_deref()
.map(|s| {
let t = s.trim();
t != "@" && t != "*" && !t.contains(',')
})
.unwrap_or(false);
let is_at_star = matches!(subscript.as_deref(), Some("@") | Some("*"));
let per_element_array = !has_scalar_sub && (!qt || is_at_star || nojoin == 2);
let match_only = (sub_flags_get() & SUB_MATCH) != 0;
let strip_one = |val: &str, op: u8| -> String {
let cv: Vec<char> = val.chars().collect();
let nn = cv.len();
match op {
1 => {
let mut k = nn;
loop {
let prefix: String = cv[..k].iter().collect();
if crate::vm_helper::glob_match_static(&prefix, &p) {
if match_only {
return prefix;
}
return cv[k..].iter().collect();
}
if k == 0 {
break;
}
k -= 1;
}
if match_only {
String::new()
} else {
val.to_string()
}
}
_ => val.to_string(),
}
};
if let Some(arr) = arrays_get(&var_name).filter(|_| per_element_array) {
let new_arr: Vec<String> = arr.iter().map(|e| strip_one(e, 1)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr); } else {
value = strip_one(&raw_value, 1); }
} else if let Some(pat) = r.strip_prefix('#') {
let p = singsub(pat);
let has_scalar_sub = subscript
.as_deref()
.map(|s| {
let t = s.trim();
t != "@" && t != "*" && !t.contains(',')
})
.unwrap_or(false);
let is_at_star = matches!(subscript.as_deref(), Some("@") | Some("*"));
let per_element_array = !has_scalar_sub && (!qt || is_at_star || nojoin == 2);
let match_only = (sub_flags_get() & SUB_MATCH) != 0;
let strip_one = |val: &str| -> String {
let cv: Vec<char> = val.chars().collect();
let total = cv.len();
for k in 0..=total {
let prefix: String = cv[..k].iter().collect();
if crate::vm_helper::glob_match_static(&prefix, &p) {
if match_only {
return prefix;
}
return cv[k..].iter().collect();
}
}
if match_only {
String::new()
} else {
val.to_string()
}
};
if let Some(arr) = arrays_get(&var_name).filter(|_| per_element_array) {
let new_arr: Vec<String> = arr.iter().map(|e| strip_one(e)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr); } else {
value = strip_one(&raw_value); }
} else if let Some(pat) = r.strip_prefix("%%") {
let p = singsub(pat);
let has_scalar_sub = subscript
.as_deref()
.map(|s| {
let t = s.trim();
t != "@" && t != "*" && !t.contains(',')
})
.unwrap_or(false);
let is_at_star = matches!(subscript.as_deref(), Some("@") | Some("*"));
let per_element_array = !has_scalar_sub && (!qt || is_at_star || nojoin == 2);
let match_only = (sub_flags_get() & SUB_MATCH) != 0;
let strip_one = |val: &str| -> String {
let cv: Vec<char> = val.chars().collect();
let total = cv.len();
let mut k = total;
loop {
let suffix_start_char = total - k;
let suffix: String = cv[suffix_start_char..].iter().collect();
if crate::vm_helper::glob_match_static(&suffix, &p) {
let suffix_byte_off: usize =
cv[..suffix_start_char].iter().map(|c| c.len_utf8()).sum();
if suffix_byte_off > 0 {
if let Some(b) = crate::ported::params::getaparam("mbegin") {
let shifted: Vec<String> = b
.iter()
.map(|s| {
s.parse::<i64>()
.map(|n| (n + suffix_byte_off as i64).to_string())
.unwrap_or_else(|_| s.clone())
})
.collect();
crate::ported::params::setaparam("mbegin", shifted);
}
if let Some(e) = crate::ported::params::getaparam("mend") {
let shifted: Vec<String> = e
.iter()
.map(|s| {
s.parse::<i64>()
.map(|n| (n + suffix_byte_off as i64).to_string())
.unwrap_or_else(|_| s.clone())
})
.collect();
crate::ported::params::setaparam("mend", shifted);
}
}
if match_only {
return suffix;
}
return cv[..suffix_start_char].iter().collect();
}
if k == 0 {
break;
}
k -= 1;
}
if match_only {
String::new()
} else {
val.to_string()
}
};
if let Some(arr) = arrays_get(&var_name).filter(|_| per_element_array) {
let new_arr: Vec<String> = arr.iter().map(|e| strip_one(e)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr); } else {
value = strip_one(&raw_value); }
} else if let Some(pat) = r.strip_prefix('%') {
let p = singsub(pat);
let has_scalar_sub = subscript
.as_deref()
.map(|s| {
let t = s.trim();
t != "@" && t != "*" && !t.contains(',')
})
.unwrap_or(false);
let is_at_star = matches!(subscript.as_deref(), Some("@") | Some("*"));
let per_element_array = !has_scalar_sub && (!qt || is_at_star || nojoin == 2);
let match_only = (sub_flags_get() & SUB_MATCH) != 0;
let strip_one = |val: &str| -> String {
let cv: Vec<char> = val.chars().collect();
let total = cv.len();
for k in 0..=total {
let suffix: String = cv[total - k..].iter().collect();
if crate::vm_helper::glob_match_static(&suffix, &p) {
if match_only {
return suffix;
}
return cv[..total - k].iter().collect();
}
}
if match_only {
String::new()
} else {
val.to_string()
}
};
if let Some(arr) = arrays_get(&var_name).filter(|_| per_element_array) {
let new_arr: Vec<String> = arr.iter().map(|e| strip_one(e)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr); } else {
value = strip_one(&raw_value); }
} else if let Some(rhs) = r.strip_prefix(":|") {
let arr = arrays_get(&var_name).unwrap_or_default();
let other_name = rhs.trim(); let other = arrays_get(other_name).unwrap_or_default();
let other_set: std::collections::HashSet<&String> = other.iter().collect();
let kept: Vec<String> = arr
.into_iter() .filter(|s| !other_set.contains(s)) .collect();
value = kept.join(" ");
split_parts = Some(kept); isarr = 1;
} else if let Some(rhs) = r.strip_prefix(":*") {
let arr = arrays_get(&var_name).unwrap_or_default();
let other_name = rhs.trim(); let other = arrays_get(other_name).unwrap_or_default();
let other_set: std::collections::HashSet<&String> = other.iter().collect();
let kept: Vec<String> = arr
.into_iter() .filter(|s| other_set.contains(s)) .collect();
value = kept.join(" ");
split_parts = Some(kept); isarr = 1; } else if let Some(rhs) = r.strip_prefix(":^^") {
let arr = arrays_get(&var_name)
.or_else(|| vars_get(&var_name).map(|s| vec![s]))
.unwrap_or_default();
let other_name = rhs.trim();
let other = arrays_get(other_name)
.or_else(|| vars_get(other_name).map(|s| vec![s]))
.unwrap_or_default();
let zipped: Vec<String> = if qt && !other.is_empty() && !arr.is_empty() {
let ifs0 = vars_get("IFS")
.unwrap_or_else(|| " \t\n\0".to_string())
.chars()
.next()
.map(String::from)
.unwrap_or_default();
let joined = arr.join(&ifs0);
let n = other.len();
let mut z: Vec<String> = Vec::with_capacity(n * 2);
for i in 0..n {
z.push(joined.clone());
z.push(other[i].clone());
}
z
} else if arr.is_empty() {
other.clone()
} else if other.is_empty() {
arr.clone()
} else {
let n = arr.len().max(other.len());
let mut z: Vec<String> = Vec::with_capacity(n * 2);
for i in 0..n {
z.push(arr[i % arr.len()].clone());
z.push(other[i % other.len()].clone());
}
z
};
value = zipped.join(" ");
split_parts = Some(zipped); isarr = 1; } else if let Some(rhs) = r.strip_prefix(":^") {
let arr_opt =
arrays_get(&var_name).or_else(|| vars_get(&var_name).map(|s| vec![s]));
let other_name = rhs.trim();
let other_opt =
arrays_get(other_name).or_else(|| vars_get(other_name).map(|s| vec![s]));
let arr_unset = arr_opt.is_none();
let other_unset = other_opt.is_none();
let arr = arr_opt.unwrap_or_default();
let other = other_opt.unwrap_or_default();
let zipped: Vec<String> = if other_unset && !arr_unset {
arr.clone()
} else if arr_unset && !other_unset {
other.clone()
} else if qt {
let ifs0 = vars_get("IFS")
.unwrap_or_else(|| " \t\n\0".to_string())
.chars()
.next()
.map(String::from)
.unwrap_or_default();
let joined = arr.join(&ifs0);
if other.is_empty() {
Vec::new()
} else {
vec![joined, other[0].clone()]
}
} else {
let mut z: Vec<String> = Vec::with_capacity(arr.len() + other.len());
let n = arr.len().min(other.len()); for i in 0..n {
z.push(arr[i].clone());
z.push(other[i].clone());
}
z
};
value = zipped.join(" ");
split_parts = Some(zipped); isarr = 1; } else if let Some(slice) = r.strip_prefix(':') {
let first = slice.chars().next().unwrap_or('\0');
let is_modifier = matches!(
first,
'h' | 't'
| 'r'
| 'e'
| 'l'
| 'u'
| 'q'
| 'Q'
| 'A'
| 'a'
| 'P'
| 'c'
| 's'
| 'S'
| '&'
| 'g'
| 'w'
| 'W'
| 'f'
| 'F'
);
if !is_modifier && first.is_ascii_alphabetic() {
zerr(&format!("unrecognized modifier `{}'", first)); errflag_set_error();
return (String::new(), 0, Vec::new()); }
if is_modifier {
let mod_str = format!(":{}", slice);
let mod_one = |s: &str| -> String { modify(s, &mod_str) };
let sepjoined_for_qt = || -> String {
if let Some(arr) = arrays_get(&var_name) {
arr.join(" ") } else {
value.clone()
}
};
let is_at = subscript.as_deref() == Some("@");
let is_star = matches!(subscript.as_deref(), Some("*") | Some("\u{87}"));
let is_range = subscript.as_deref().map_or(false, |s| s.contains(','));
let per_element = is_at || ((is_star || is_range) && !qt);
let scalar_subscript = subscript.is_some() && !per_element;
if scalar_subscript {
let _ = value.clone();
value = mod_one(&value);
if (is_star || is_range) && qt {
split_parts = Some(vec![value.clone()]);
isarr = 0;
}
} else if per_element {
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| mod_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let narrowed: Vec<String> = if is_range {
if let Some((lo_s, hi_s)) =
subscript.as_deref().and_then(|s| s.split_once(','))
{
let lo: i64 = lo_s.trim().parse().unwrap_or(1);
let hi: i64 = hi_s.trim().parse().unwrap_or(arr.len() as i64);
getarrvalue(&arr, lo, hi)
} else {
arr.clone()
}
} else {
arr.clone()
};
let new_arr: Vec<String> =
narrowed.iter().map(|s| mod_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = mod_one(&value);
}
} else if qt && arrays_contains(&var_name) {
value = mod_one(&sepjoined_for_qt());
split_parts = None;
} else if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| mod_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| mod_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = mod_one(&value);
}
} else {
let parts: Vec<&str> = slice.splitn(2, ':').collect();
let off = crate::ported::math::mathevali(&singsub(parts[0])).unwrap_or(0);
let single_slot_subscript = subscript
.as_deref()
.map_or(false, |s| s != "@" && s != "*" && !s.contains(','));
let array_source: Option<Vec<String>> = if single_slot_subscript {
None } else {
split_parts.clone().or_else(|| arrays_get(&var_name))
};
if let Some(mut arr) = array_source {
if var_name == "@" || var_name == "*" || var_name == "argv" {
let s0 = vars_get("0").unwrap_or_default();
arr.insert(0, s0); }
let n = arr.len() as i64; let lo = if off < 0 {
(n + off).max(0)
} else {
off.min(n)
} as usize; let len = parts
.get(1) .map(|s| crate::ported::math::mathevali(&singsub(s)).unwrap_or(0)); let kept: Vec<String> = match len {
Some(l) if l >= 0 => {
arr.iter().skip(lo).take(l as usize).cloned().collect()
} Some(l) => {
let end = ((n - lo as i64) + l).max(0) as usize; arr.iter().skip(lo).take(end).cloned().collect()
} None => arr.iter().skip(lo).cloned().collect(), };
value = kept.join(" "); split_parts = Some(kept); } else {
let total = raw_value.chars().count() as i64;
let start = if off < 0 {
(total + off).max(0)
} else {
off.min(total)
} as usize;
let len = parts
.get(1)
.map(|s| crate::ported::math::mathevali(&singsub(s)).unwrap_or(0));
value = match len {
Some(l) if l >= 0 => {
raw_value.chars().skip(start).take(l as usize).collect()
}
Some(l) => {
let take = ((total - start as i64) + l).max(0) as usize;
raw_value.chars().skip(start).take(take).collect()
}
None => raw_value.chars().skip(start).collect(),
};
}
} } else if r.starts_with('^') || r.starts_with(',') {
zerr("bad substitution");
errflag.fetch_or(
crate::ported::zsh_h::ERRFLAG_ERROR,
std::sync::atomic::Ordering::Relaxed,
);
value = String::new();
}
}
if wantt {
let partab_array_tag = crate::vm_helper::partab_array_flags(&var_name).map(|f| {
let mut tag = if f & PM_HASHED != 0 {
"association".to_string()
} else if f & PM_ARRAY != 0 {
"array".to_string()
} else if f & PM_INTEGER != 0 {
"integer".to_string()
} else {
"scalar".to_string()
};
if f & PM_READONLY != 0 {
tag.push_str("-readonly");
}
if f & PM_TAGGED != 0 {
tag.push_str("-tag");
}
if f & PM_TIED != 0 {
tag.push_str("-tied");
}
if f & PM_EXPORTED != 0 {
tag.push_str("-export");
}
if f & PM_UNIQUE != 0 {
tag.push_str("-unique");
}
if f & PM_HIDE != 0 {
tag.push_str("-hide");
}
if f & PM_HIDEVAL != 0 {
tag.push_str("-hideval");
}
if f & PM_SPECIAL != 0 {
tag.push_str("-special");
}
tag
});
value = if let Some(tag) = partab_array_tag {
tag
} else {
paramtab()
.read() .ok() .and_then(|tab| {
tab.get(&var_name).map(|pm| {
let f = pm.node.flags as u32; let f_overlay = if (f & PM_SPECIAL) != 0 {
let mut bits = f;
if let Some(sp) = crate::ported::params::special_params
.iter()
.find(|sp| sp.name == var_name.as_str())
{
bits |= sp.pm_type as u32;
bits |= sp.pm_flags as u32;
}
if std::env::var(&var_name).is_ok() {
bits |= PM_EXPORTED;
}
bits
} else {
f
};
let f = f_overlay;
let val = if f & PM_HASHED != 0 {
"association"
}
else if f & PM_ARRAY != 0 {
"array"
}
else if f & PM_INTEGER != 0 {
"integer"
}
else if f & (PM_EFLOAT | PM_FFLOAT) != 0 {
"float"
}
else if f & PM_NAMEREF != 0 {
"nameref"
}
else {
"scalar"
}; let val = dupstring(val); let val = if pm.level != 0
{
dyncat(&val, "-local")
}
else {
val
}; let val = if f & PM_LEFT != 0
{
dyncat(&val, "-left")
}
else {
val
}; let val = if f & PM_RIGHT_B != 0
{
dyncat(&val, "-right_blanks")
}
else {
val
}; let val = if f & PM_RIGHT_Z != 0
{
dyncat(&val, "-right_zeros")
}
else {
val
}; let val = if f & PM_LOWER != 0
{
dyncat(&val, "-lower")
}
else {
val
}; let val = if f & PM_UPPER != 0
{
dyncat(&val, "-upper")
}
else {
val
}; let val = if f & PM_READONLY != 0
{
dyncat(&val, "-readonly")
}
else {
val
}; let val = if f & PM_TAGGED != 0
{
dyncat(&val, "-tag")
}
else {
val
}; let val = if f & PM_TIED != 0
{
dyncat(&val, "-tied")
}
else {
val
}; let val = if f & PM_EXPORTED != 0
{
dyncat(&val, "-export")
}
else {
val
}; let val = if f & PM_UNIQUE != 0
{
dyncat(&val, "-unique")
}
else {
val
}; let val = if f & PM_HIDE != 0
{
dyncat(&val, "-hide")
}
else {
val
}; let val = if f & PM_HIDEVAL != 0
{
dyncat(&val, "-hideval")
}
else {
val
}; let val = if f & PM_SPECIAL != 0
{
dyncat(&val, "-special")
}
else {
val
}; val })
})
.unwrap_or_else(|| {
if let Some(f) = crate::vm_helper::partab_array_flags(&var_name) {
let mut tag = if f & PM_HASHED != 0 {
"association".to_string()
} else if f & PM_ARRAY != 0 {
"array".to_string()
} else if f & PM_INTEGER != 0 {
"integer".to_string()
} else {
"scalar".to_string()
};
if f & PM_READONLY != 0 {
tag.push_str("-readonly");
}
if f & PM_TAGGED != 0 {
tag.push_str("-tag");
}
if f & PM_TIED != 0 {
tag.push_str("-tied");
}
if f & PM_EXPORTED != 0 {
tag.push_str("-export");
}
if f & PM_UNIQUE != 0 {
tag.push_str("-unique");
}
if f & PM_HIDE != 0 {
tag.push_str("-hide");
}
if f & PM_HIDEVAL != 0 {
tag.push_str("-hideval");
}
if f & PM_SPECIAL != 0 {
tag.push_str("-special");
}
return tag;
}
if assoc_contains(&var_name) {
"association".to_string() } else if arrays_contains(&var_name) {
"array".to_string() } else if matches!(
var_name.as_str(),
"aliases"
| "galiases"
| "saliases"
| "dis_aliases"
| "dis_galiases"
| "dis_saliases"
| "functions"
| "dis_functions"
| "builtins"
| "dis_builtins"
| "reswords"
| "dis_reswords"
| "options"
| "commands"
| "modules"
| "nameddirs"
| "userdirs"
| "jobtexts"
| "jobdirs"
| "jobstates"
| "parameters"
| "dirstack"
| "errnos"
| "sysparams"
| "mapfile"
) {
"association".to_string() } else if is_set {
if std::env::var(&var_name).is_ok() {
"scalar-export".to_string()
} else {
"scalar".to_string()
}
} else {
String::new()
}
})
};
isarr = 0; split_parts = None; }
let cap_word = |s: &str| -> String {
let mut out = String::with_capacity(s.len());
let mut next_upper = true;
for c in s.chars() {
if c.is_whitespace() || matches!(c, '-' | '_' | '/' | '.' | ',') {
out.push(c);
next_upper = true;
} else if next_upper {
out.extend(c.to_uppercase());
next_upper = false;
} else {
out.extend(c.to_lowercase());
}
}
out
};
if casmod != CASMOD_NONE {
let transform = |s: &str| -> String {
if casmod == CASMOD_LOWER {
s.to_lowercase() } else if casmod == CASMOD_UPPER {
s.to_uppercase() } else {
cap_word(s) } }; if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| transform(s)).collect();
value = new_parts.join(" "); split_parts = Some(new_parts); } else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| transform(s)).collect();
value = new_arr.join(" "); split_parts = Some(new_arr); } else {
value = transform(&value); }
}
if isarr != 0 && (sortit != SORTIT_ANYOLDHOW || unique) && sep.is_none() {
let parts: Vec<String> = if let Some(sp) = split_parts.clone() {
sp } else if let Some(arr) = arrays_get(&var_name) {
arr.clone() } else if let Some(map) = assoc_get(&var_name) {
map.values().cloned().collect() } else {
value.split_whitespace().map(String::from).collect() }; let mut sorted: Vec<String> = parts; if unique {
let mut seen = std::collections::HashSet::new(); sorted.retain(|s| seen.insert(s.clone())); } if sortit != SORTIT_ANYOLDHOW {
if indord == 0 {
if (sortit & SORTIT_NUMERICALLY) != 0 {
let flags = sortit as u32;
sorted.sort_by(|a, b| crate::ported::sort::zstrcmp(a, b, flags));
} else if (sortit & SORTIT_IGNORING_CASE) != 0 {
sorted.sort_by_key(|a| a.to_lowercase()); } else {
sorted.sort_by(|a, b| {
crate::ported::sort::zstrcmp(a, b, 0) });
} } if (sortit & SORTIT_BACKWARDS) != 0 {
sorted.reverse(); } } let join_with = sep.as_deref().unwrap_or(" "); value = sorted.join(join_with); split_parts = Some(sorted); }
if let Some(ref sp) = spsep {
let split_one = |s: &str| -> Vec<String> {
if sp.is_empty() {
s.chars().map(|c| c.to_string()).collect()
} else {
s.split(sp.as_str()).map(String::from).collect()
}
};
let parts: Vec<String> = if let Some(prev) = split_parts.clone() {
prev.iter().flat_map(|s| split_one(s)).collect()
} else if let Some(arr) = arrays_get(&var_name) {
arr.iter().flat_map(|s| split_one(s)).collect()
} else {
split_one(&value)
};
let join_with = sep.as_deref().unwrap_or(" "); value = parts.join(join_with);
split_parts = Some(parts); isarr = if nojoin != 0 { 1 } else { 2 }; } else if let Some(ref sp) = sep {
let mut joined = false;
if let Some(parts) = split_parts.clone() {
value = parts.join(sp); split_parts = None; joined = true;
} else if let Some(arr) = arrays_get(&var_name) {
value = arr.join(sp); joined = true;
} else if let Some(map) = assoc_get(&var_name) {
let vals: Vec<String> = map.values().cloned().collect();
value = vals.join(sp); joined = true;
}
if joined {
isarr = 0; }
}
if prenum > 0 || postnum > 0 {
let mul_default = " ".to_string(); let pad_one = |s: &str| -> String {
dopadding(
s,
prenum.max(0) as usize,
postnum.max(0) as usize,
preone.as_deref(),
postone.as_deref(),
premul.as_deref().unwrap_or(&mul_default),
postmul.as_deref().unwrap_or(&mul_default),
multi_width as i32, )
};
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| pad_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| pad_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = pad_one(&value);
}
}
if evalchar {
let eval_one = |s: &str| -> String { substevalchar(s.trim()).unwrap_or_default() };
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| eval_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| eval_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = eval_one(&value);
}
}
if eval {
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| singsub(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| singsub(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = singsub(&value); }
}
if presc > 0 {
let prompt_one = |s: &str| -> String {
let (expanded, _, _) = promptexpand(s, 0, None);
expanded
};
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| prompt_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| prompt_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = prompt_one(&value); }
}
if (shsplit & LEXFLAGS_ACTIVE) != 0 {
let mut words: Vec<String> = Vec::new(); let mut cur = String::new(); let mut in_sq = false; let mut in_dq = false; let mut in_comment = false; let chars_v: Vec<char> = value.chars().collect(); let push_word = |w: &mut String, words: &mut Vec<String>| {
if !w.is_empty() {
words.push(std::mem::take(w)); } }; let mut p = 0_usize; while p < chars_v.len() {
let ch = chars_v[p]; if in_comment {
if ch == '\n' {
in_comment = false; if (shsplit & LEXFLAGS_COMMENTS_KEEP) != 0 {
cur.push(ch);
} } else if (shsplit & LEXFLAGS_COMMENTS_KEEP) != 0 {
cur.push(ch); } p += 1; continue; } if in_sq {
cur.push(ch); if ch == '\'' {
in_sq = false;
} p += 1;
continue; } if in_dq {
cur.push(ch); if ch == '\\' && p + 1 < chars_v.len() {
p += 1; cur.push(chars_v[p]); } else if ch == '"' {
in_dq = false; } p += 1;
continue; } if cur.is_empty() && ch == '(' && p + 1 < chars_v.len() && chars_v[p + 1] == '(' {
let start = p;
let mut depth = 2_i32;
p += 2;
while p < chars_v.len() && depth > 0 {
let pch = chars_v[p];
if pch == '\\' && p + 1 < chars_v.len() {
p += 2;
continue;
}
if pch == '\'' {
p += 1;
while p < chars_v.len() && chars_v[p] != '\'' {
p += 1;
}
if p < chars_v.len() {
p += 1;
}
continue;
}
if pch == '"' {
p += 1;
while p < chars_v.len() && chars_v[p] != '"' {
if chars_v[p] == '\\' && p + 1 < chars_v.len() {
p += 2;
} else {
p += 1;
}
}
if p < chars_v.len() {
p += 1;
}
continue;
}
if pch == '(' {
depth += 1;
} else if pch == ')' {
depth -= 1;
}
p += 1;
}
let token: String = chars_v[start..p].iter().collect();
words.push(token);
continue;
}
match ch {
'\\' if p + 1 < chars_v.len() => {
cur.push(ch); p += 1; cur.push(chars_v[p]); } '\'' => {
cur.push(ch);
in_sq = true;
} '"' => {
cur.push(ch);
in_dq = true;
} '#' if cur.is_empty() && !(shsplit & LEXFLAGS_COMMENTS_STRIP) != 0 => {
in_comment = !(shsplit & LEXFLAGS_COMMENTS_KEEP) != 0; if (shsplit & LEXFLAGS_COMMENTS_KEEP) != 0 {
cur.push(ch);
} } '#' if cur.is_empty() && (shsplit & LEXFLAGS_COMMENTS_STRIP) != 0 => {
in_comment = true; } '\n' if (shsplit & LEXFLAGS_NEWLINE) != 0 => {
push_word(&mut cur, &mut words); } ';' | '&' | '\n' => {
push_word(&mut cur, &mut words);
let canon = if ch == '\n' { ';' } else { ch };
let mut sep_str = String::from(canon);
if (ch == '&' || ch == ';') && p + 1 < chars_v.len() && chars_v[p + 1] == ch
{
sep_str.push(ch);
p += 1;
}
words.push(sep_str);
}
'|' => {
push_word(&mut cur, &mut words);
let mut sep_str = String::from(ch);
if p + 1 < chars_v.len() && chars_v[p + 1] == '|' {
sep_str.push('|');
p += 1;
}
words.push(sep_str);
}
c if c.is_whitespace() => {
push_word(&mut cur, &mut words); } _ => cur.push(ch), } p += 1; } push_word(&mut cur, &mut words); value = words.join(" "); if !words.is_empty() {
split_parts = Some(words); isarr = if nojoin != 0 { 1 } else { 2 }; }
}
if (mods & 1) != 0 {
let home_opt = getsparam("HOME"); let mut named: Vec<(String, String)> = nameddirtab()
.lock()
.map(|t| {
t.iter()
.map(|(k, nd)| (k.clone(), nd.dir.clone()))
.collect()
})
.unwrap_or_default();
named.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
let dir_one = |s: &str| -> String {
if let Some(ref h) = home_opt {
if !h.is_empty() && s.starts_with(h.as_str()) {
let r = &s[h.len()..];
if r.is_empty() || r.starts_with('/') {
return format!("~{}", r);
}
}
}
for (name, path) in &named {
if !path.is_empty() && s.starts_with(path.as_str()) {
let r = &s[path.len()..];
if r.is_empty() || r.starts_with('/') {
return format!("~{}{}", name, r);
}
}
}
s.to_string()
};
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| dir_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| dir_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = dir_one(&value); }
}
let b_one = |s: &str| -> String {
let mut out = String::with_capacity(s.len() * 2);
for ch in s.chars() {
if matches!(
ch,
'*' | '?'
| '['
| ']'
| '('
| ')'
| '|'
| '^'
| '#'
| '~'
| '\\'
| '<'
| '>'
| '&'
| ';'
| '{'
| '}'
| '$'
| '`'
| '"'
| '\''
| ' '
| '\t'
| '\n'
) {
out.push('\\');
}
out.push(ch);
}
out
};
if quotemod > 0 && quotetype == QT_BACKSLASH_PATTERN {
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| b_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| b_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = b_one(&value); } }
let unquote_one = |s: &str| -> String {
let chars_v: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0_usize;
while i < chars_v.len() {
let c = chars_v[i];
if c == '$' && i + 1 < chars_v.len() && chars_v[i + 1] == '\'' {
let body_start = i + 2;
let mut j = body_start;
while j < chars_v.len() && chars_v[j] != '\'' {
if chars_v[j] == '\\' && j + 1 < chars_v.len() {
j += 2;
} else {
j += 1;
}
}
let body: String = chars_v[body_start..j].iter().collect();
let (decoded, _) = getkeystring(&body); out.push_str(&decoded);
i = j + 1;
continue;
}
if c == '\\' {
if i + 1 < chars_v.len() {
out.push(chars_v[i + 1]);
i += 2;
continue;
}
} else if c == '\'' || c == '"' {
i += 1;
continue;
}
out.push(c);
i += 1;
}
out
};
if quotemod < 0 {
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| unquote_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| unquote_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = unquote_one(&value);
}
}
if quoteerr && value.is_empty() && !is_set {
zerr(&format!("{}: parameter not set or null", var_name)); errflag_set_error();
}
let visible_one = |s: &str| -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
out.push_str(&crate::ported::utils::nicechar(c)); }
out
};
if (mods & 2) != 0 {
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| visible_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| visible_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = visible_one(&value);
}
}
if length_op && whichlen == 1 {
value = if multi_width > 0 {
value .chars() .map(|c| wcpadwidth(c, multi_width as i32) as usize) .sum::<usize>() .to_string() } else {
value.chars().count().to_string() }; } else if length_op && whichlen == 2 {
value = value.split_whitespace().count().to_string(); } else if length_op && whichlen == 3 {
let parts: Vec<&str> = value.split(|c: char| c.is_whitespace()).collect(); value = parts.len().to_string(); }
let quote_one = |s: &str| -> String {
if quotetype == QT_SINGLE_OPTIONAL {
if s.is_empty() {
return "''".to_string(); }
quotestring(s, QT_SINGLE_OPTIONAL) } else if quotetype == QT_QUOTEDZPUTS {
crate::ported::utils::quotedzputs(s) } else if quotemod > 0 {
quotestring(s, quotetype) } else {
s.to_string() } }; if quotemod > 0 && quotetype != QT_BACKSLASH_PATTERN {
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| quote_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| quote_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = quote_one(&value);
}
}
if getkeys >= 0 {
let decode_one = |s: &str| -> String { getkeystring(s).0 };
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| decode_one(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| decode_one(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = decode_one(&value);
}
}
if mods != 0 {
let render_d = |s: &str| -> String {
if (mods & 1) == 0 {
return s.to_string(); } crate::ported::utils::finddir(s).unwrap_or_else(|| s.to_string())
};
let render_v = |s: &str| -> String {
if (mods & 2) == 0 {
return s.to_string(); } let mut out = String::with_capacity(s.len());
for ch in s.chars() {
let code = ch as u32;
if (0x20..=0x7e).contains(&code) {
out.push(ch);
} else if code == 0x7f {
out.push('^');
out.push('?');
} else if code == 0x0a {
out.push('\\');
out.push('n');
} else if code == 0x09 {
out.push('\\');
out.push('t');
} else if code < 0x20 {
out.push('^');
out.push((b'@' + (code as u8)) as char);
} else if code < 0x100 {
out.push_str("\\M-");
let stripped = code & 0x7f;
if (0x20..=0x7e).contains(&stripped) {
out.push(stripped as u8 as char);
} else if stripped < 0x20 {
out.push('^');
out.push((b'@' + (stripped as u8)) as char);
} else {
out.push('?');
}
} else {
out.push(ch);
}
}
out
};
let pipeline = |s: &str| -> String {
let s1 = render_d(s);
render_v(&s1)
};
if let Some(parts) = split_parts.clone() {
let new_parts: Vec<String> = parts.iter().map(|s| pipeline(s)).collect();
value = new_parts.join(" ");
split_parts = Some(new_parts);
} else if let Some(arr) = arrays_get(&var_name) {
let new_arr: Vec<String> = arr.iter().map(|s| pipeline(s)).collect();
value = new_arr.join(" ");
split_parts = Some(new_arr);
} else {
value = pipeline(&value);
}
}
let in_ssub = pf_flags & PREFORK_SINGLE != 0;
if force_split && !in_ssub && split_parts.is_none() {
let ifs = vars_get("IFS").unwrap_or_else(|| " \t\n".to_string());
let parts: Vec<String> = value
.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
if !parts.is_empty() {
value = parts.join(" ");
split_parts = Some(parts);
isarr = if nojoin != 0 { 1 } else { 2 };
}
}
let _ = suppress_split;
let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = if new_pos < chars.len() {
chars[new_pos..].iter().collect() } else {
String::new() };
let scripted_scalar = subscript
.as_deref() .map(|s| s != "@" && s != "*" && !s.contains(','))
.unwrap_or(false); let force_splat_from_eq = force_split
&& pf_flags & PREFORK_SINGLE == 0
&& rest.is_empty()
&& split_parts.is_some();
let auto_splat = !wantt
&& (isarr != 0 || force_splat_from_eq || (!(nojoin == 2) && !qt && pf_flags & PREFORK_SINGLE == 0 && rest.is_empty() && !scripted_scalar && sep.is_none() && (arrays_contains(&var_name) || split_parts.is_some()))); if (nojoin == 2) || auto_splat {
let parts: Vec<String> = if let Some(sp) = split_parts.clone() {
if !qt && spsep.is_some() {
sp.into_iter().filter(|s| !s.is_empty()).collect()
} else {
sp }
} else if let Some(sub) = subscript.as_deref() {
if let Some((lo, hi)) = sub.split_once(',') {
let lo: i64 = lo.trim().parse().unwrap_or(1); let hi: i64 = hi.trim().parse().unwrap_or(0); arrays_get(&var_name)
.as_ref() .map(|arr| getarrvalue(arr, lo, hi))
.unwrap_or_default()
} else if let Some(arr) = arrays_get(&var_name) {
arr.clone() } else {
vec![value.clone()]
}
} else if let Some(arr) = arrays_get(&var_name) {
arr.clone() } else if let Some(map) = assoc_get(&var_name) {
if (hkeys & SCANPM_WANTKEYS) != 0 && (hvals & SCANPM_WANTVALS) != 0 {
let mut out: Vec<String> = Vec::with_capacity(map.len() * 2); for (k, v) in map {
out.push(k.clone()); out.push(v.clone()); } out } else if (hkeys & SCANPM_WANTKEYS) != 0 {
map.keys().cloned().collect()
} else if (hvals & SCANPM_WANTVALS) != 0 {
map.values().cloned().collect()
} else {
vec![value.clone()] }
} else {
vec![value.clone()] };
let nul_str = "\u{a1}";
let emit_part = |s: &str| -> String {
if s.is_empty() {
nul_str.to_string()
} else {
s.to_string()
}
};
let mut nodes: Vec<String> = Vec::with_capacity(parts.len());
for (i, part) in parts.iter().enumerate() {
let s = if parts.len() == 1 {
format!("{}{}{}", prefix, emit_part(part), suffix)
} else if i == 0 {
format!("{}{}", prefix, emit_part(part))
} else if i == parts.len() - 1 {
format!("{}{}", emit_part(part), suffix)
} else {
emit_part(part)
};
nodes.push(s);
}
let first = nodes.first().cloned().unwrap_or_default();
let new_pos_in_full = prefix.chars().count()
+ first.chars().count().saturating_sub(prefix.chars().count());
return (first, new_pos_in_full, nodes);
}
let full = format!("{}{}{}", prefix, value, suffix); let new_pos_in_full = prefix.chars().count() + value.chars().count();
return (full.clone(), new_pos_in_full, vec![full]);
}
if c == '+' {
let name_start = pos + 1;
let mut name_end = name_start;
if name_end < chars.len() {
let first = chars[name_end];
if first.is_ascii_alphanumeric() || first == '_' {
while name_end < chars.len()
&& (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '_')
{
name_end += 1;
}
} else if matches!(first, '@' | '*' | '#' | '?') {
name_end += 1;
}
}
if name_end > name_start {
let mut sub_end = name_end;
if chars.get(sub_end).copied() == Some('[') {
let mut depth = 1;
let mut q = sub_end + 1;
while q < chars.len() && depth > 0 {
match chars[q] {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
q += 1;
}
if depth == 0 && q < chars.len() && chars[q] == ']' {
sub_end = q + 1;
}
}
let name_with_sub: String = chars[name_start..sub_end].iter().collect();
let prefix: String = chars[..start_pos].iter().collect();
let suffix: String = chars[sub_end..].iter().collect();
let rewritten = format!("{}${{+{}}}{}", prefix, name_with_sub, suffix);
return paramsubst(&rewritten, prefix.chars().count(), qt, pf_flags, ret_flags);
}
}
if c.is_ascii_alphabetic() || c == '_' {
let var_start = pos; while pos < chars.len() && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
pos += 1; } let var_name: String = chars[var_start..pos].iter().collect();
let mut subscript_str: Option<String> = None; let opener = chars.get(pos).copied();
if opener == Some('[') || opener == Some(Inbrack) {
let mut depth = 1; let mut q = pos + 1; while q < chars.len() && depth > 0 {
let ch = chars[q];
if ch == '[' || ch == Inbrack {
depth += 1;
} else if ch == ']' || ch == Outbrack {
depth -= 1;
if depth == 0 {
break;
}
}
q += 1; } if depth == 0 {
let raw_sub: String = chars[pos + 1..q].iter().collect(); subscript_str = Some(singsub(&raw_sub)); pos = q + 1; } }
let value = if let Some(sub) = subscript_str.as_deref() {
if let Some(map) = assoc_get(&var_name) {
if let Some((flags, pat)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let f = rest[..close].to_string();
let p = rest[close + 1..].to_string();
if f.chars()
.all(|c| matches!(c, 'I' | 'R' | 'i' | 'r' | 'k' | 'K' | 'n' | 'e' | 'b'))
{
Some((f, p))
} else {
None
}
})(sub)
{
let by_key = flags.contains('I') || flags.contains('i');
let return_all = flags.contains('I') || flags.contains('R');
let exact = flags.contains('e'); let mut out: Vec<String> = Vec::new();
for (k, v) in map.iter() {
let hay = if by_key { k.as_str() } else { v.as_str() };
let matched = if exact {
hay == pat.as_str()
} else {
patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, hay))
};
if matched {
out.push(if by_key { k.clone() } else { v.clone() });
if !return_all {
break;
}
}
}
out.join(" ")
} else {
map.get(sub).cloned().unwrap_or_default() }
} else if let Some(arr) = arrays_get(&var_name) {
if sub == "*" || sub == "@" {
arr.join(" ") } else if let Some((flags, pat)) = (|s: &str| -> Option<(String, String)> {
let s = s.trim_start();
let rest = s.strip_prefix('(')?;
let close = rest.find(')')?;
let f = rest[..close].to_string();
let p = rest[close + 1..].to_string();
if f.chars()
.all(|c| matches!(c, 'I' | 'R' | 'i' | 'r' | 'n' | 'e'))
{
Some((f, p))
} else {
None
}
})(sub)
{
let return_index = flags.contains('I') || flags.contains('i');
let return_all = flags.contains('I') || flags.contains('R');
let mut out: Vec<String> = Vec::new();
for (idx, elem) in arr.iter().enumerate() {
if patcompile(&pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, elem))
{
if return_index {
out.push((idx + 1).to_string());
} else {
out.push(elem.clone());
}
if !return_all {
break;
}
}
}
if out.is_empty() && return_index {
(arr.len() + 1).to_string()
} else {
out.join(" ")
}
} else if let Some((lo, hi)) = sub.split_once(',') {
let lo: i64 = lo.trim().parse().unwrap_or(1); let hi: i64 = hi.trim().parse().unwrap_or(arr.len() as i64); getarrvalue(&arr, lo, hi).join(" ") } else if let Some(idx) = sub.parse::<i32>().ok().or_else(|| {
crate::ported::math::mathevali(sub).ok().map(|v| v as i32)
}) {
let n = arr.len() as i32; let i = if idx == 0 {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
0 } else {
-1 }
} else if idx < 0 {
n + idx
} else {
idx - 1
}; if i >= 0 && (i as usize) < arr.len() {
arr[i as usize].clone() } else {
String::new() } } else {
String::new() } } else if let Some(magic_val) = {
let is_splice = sub == "@" || sub == "*";
if is_splice {
if let Some(values) = crate::vm_helper::partab_array_get(&var_name) {
Some(values.join(" "))
} else if let Some(keys) = crate::vm_helper::partab_scan_keys(&var_name) {
let vals: Vec<String> = keys
.iter()
.map(|k| crate::vm_helper::partab_get(&var_name, k).unwrap_or_default())
.collect();
Some(vals.join(" "))
} else {
None
}
} else {
crate::vm_helper::partab_get(&var_name, sub)
}
} {
magic_val
} else {
let s = vars_get(&var_name).unwrap_or_default(); let chars_v: Vec<char> = s.chars().collect(); if sub == "*" || sub == "@" {
s } else if let Some((lo, hi)) = sub.split_once(',') {
let lo: i64 = lo.trim().parse().unwrap_or(1); let hi: i64 = hi.trim().parse().unwrap_or(chars_v.len() as i64); let chars_arr: Vec<String> = chars_v.iter().map(|c| c.to_string()).collect(); getarrvalue(&chars_arr, lo, hi).concat()
} else if let Ok(idx) = sub.parse::<i32>() {
let n = chars_v.len() as i32; let i = if idx == 0 {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
0 } else {
-1 }
} else if idx < 0 {
n + idx
} else {
idx - 1
}; if i >= 0 && (i as usize) < chars_v.len() {
chars_v[i as usize].to_string() } else {
String::new() } } else {
String::new() } } } else {
exec_getsparam(&var_name)
.or_else(|| {
assoc_get(&var_name).map(|m| m.values().cloned().collect::<Vec<_>>().join(" "))
})
.unwrap_or_default() };
if pf_flags & PREFORK_SHWORDSPLIT != 0 && !qt {
let words = value
.split_whitespace()
.map(String::from)
.collect::<Vec<String>>(); if words.len() > 1 {
let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[pos..].iter().collect();
for (i, word) in words.iter().enumerate() {
if i == 0 {
result_nodes.push(format!("{}{}", prefix, word)); } else if i == words.len() - 1 {
result_nodes.push(format!("{}{}", word, suffix)); } else {
result_nodes.push(word.clone()); } } return (
result_nodes[0].clone(), prefix.len() + words[0].len(), result_nodes, ); } }
let splat_full = subscript_str.as_deref() == Some("@") || subscript_str.as_deref() == Some("*"); let splat_range = subscript_str
.as_deref()
.map(|s| s.contains(','))
.unwrap_or(false); let splat_assoc = (splat_full || splat_range) && assoc_contains(&var_name); if !qt && pf_flags & PREFORK_SINGLE == 0 && (subscript_str.is_none() || splat_full || splat_range) && (arrays_contains(&var_name) || splat_assoc)
{
let slice_arr: Option<Vec<String>> = if splat_range {
if let Some(sub) = subscript_str.as_deref() {
if let Some((lo, hi)) = sub.split_once(',') {
let lo: i64 = lo.trim().parse().unwrap_or(1); let hi: i64 = hi.trim().parse().unwrap_or(0); arrays_get(&var_name)
.as_ref()
.map(|arr| getarrvalue(arr, lo, hi))
} else {
None
}
} else {
None
}
} else {
None
};
let assoc_vals: Option<Vec<String>> = if splat_assoc {
assoc_get(&var_name) .map(|m| m.values().cloned().collect()) } else {
None
}; if let Some(arr) = slice_arr.or(assoc_vals).or_else(|| arrays_get(&var_name)) {
let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[pos..].iter().collect(); let mut nodes: Vec<String> = Vec::with_capacity(arr.len()); for (i, part) in arr.iter().enumerate() {
let s = if arr.len() == 1 {
format!("{}{}{}", prefix, part, suffix) } else if i == 0 {
format!("{}{}", prefix, part) } else if i == arr.len() - 1 {
format!("{}{}", part, suffix) } else {
part.clone() }; nodes.push(s); } let first = nodes.first().cloned().unwrap_or_default(); return (first, prefix.len(), nodes); } }
let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[pos..].iter().collect(); let result = format!("{}{}{}", prefix, value, suffix); result_nodes.push(result.clone()); return (result, prefix.len() + value.len(), result_nodes); }
match c {
'?' => {
let value = vars_get("?") .unwrap_or_else(|| "0".to_string()); let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[pos + 1..].iter().collect(); let result = format!("{}{}{}", prefix, value, suffix); result_nodes.push(result.clone()); (result, prefix.len() + value.len(), result_nodes) } '$' => {
let value = std::process::id().to_string(); let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[pos + 1..].iter().collect(); let result = format!("{}{}{}", prefix, value, suffix); result_nodes.push(result.clone()); (result, prefix.len() + value.len(), result_nodes) } c if c == '#' || c == Pound => {
let next = chars.get(pos + 1).copied().unwrap_or('\0');
let next_starts_name = next.is_ascii_alphabetic()
|| next == '_'
|| matches!(next, '@' | '*' | '?' | '!' | '-' | '0' | '$')
|| next == Quest
|| next == Bang
|| next == Dash
|| next == Star
|| next == Stringg
|| next == Pound;
if next_starts_name {
let name_start = pos + 1;
let mut name_end = name_start + 1;
let first = chars[name_start];
let is_single_special = matches!(first, '@' | '*' | '?' | '!' | '-' | '0' | '$');
if !is_single_special {
while name_end < chars.len()
&& (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '_')
{
name_end += 1;
}
}
let name: String = chars[name_start..name_end].iter().collect();
let prefix: String = chars[..start_pos].iter().collect();
let suffix: String = chars[name_end..].iter().collect();
let rewritten = format!("{}${{#{}}}{}", prefix, name, suffix);
return paramsubst(&rewritten, prefix.chars().count(), qt, pf_flags, ret_flags);
}
let value = arrays_get("@") .map(|a| a.len().to_string()) .unwrap_or_else(|| "0".to_string()); let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[pos + 1..].iter().collect(); let result = format!("{}{}{}", prefix, value, suffix); result_nodes.push(result.clone()); (result, prefix.len() + value.len(), result_nodes) } '*' | '@' => {
let mut values = arrays_get("@").unwrap_or_default(); let mut after_pos = pos + 1;
let nxt = chars.get(after_pos).copied();
if nxt == Some('[') || nxt == Some(Inbrack) {
let mut depth = 1;
let mut q = after_pos + 1;
while q < chars.len() && depth > 0 {
match chars[q] {
c if c == '[' || c == Inbrack => depth += 1,
c if c == ']' || c == Outbrack => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
q += 1;
}
if depth == 0 && q < chars.len() && (chars[q] == ']' || chars[q] == Outbrack) {
let sub: String = chars[after_pos + 1..q].iter().collect();
after_pos = q + 1;
if let Some((lo, hi)) = sub.split_once(',') {
let lo: i64 = lo.trim().parse().unwrap_or(1);
let hi: i64 = hi.trim().parse().unwrap_or(0);
values = getarrvalue(&values, lo, hi);
} else if sub == "@" || sub == "*" {
} else if let Ok(idx) = sub.parse::<i64>() {
let n = values.len() as i64;
let i = if idx == 0 {
-1
} else if idx < 0 {
n + idx
} else {
idx - 1
};
values = if i >= 0 && (i as usize) < values.len() {
vec![values[i as usize].clone()]
} else {
Vec::new()
};
}
}
}
let value = if c == '*' {
let join_sep = vars_get("IFS")
.as_ref()
.and_then(|s| s.chars().next())
.map(String::from)
.unwrap_or_else(|| " ".to_string());
values.join(&join_sep) } else {
if pf_flags & PREFORK_SINGLE == 0 {
let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[after_pos..].iter().collect(); for (i, v) in values.iter().enumerate() {
if i == 0 {
result_nodes.push(format!("{}{}", prefix, v)); } else if i == values.len() - 1 {
result_nodes.push(format!("{}{}", v, suffix)); } else {
result_nodes.push(v.clone()); } } if result_nodes.is_empty() {
result_nodes.push(format!("{}{}", prefix, suffix)); } return (result_nodes[0].clone(), start_pos, result_nodes); } values.join(" ") }; let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[after_pos..].iter().collect(); let result = format!("{}{}{}", prefix, value, suffix); result_nodes.push(result.clone()); (result, prefix.len() + value.len(), result_nodes) } '0'..='9' => {
let mut digit_str = String::from(c); let mut nx = pos + 1; while nx < chars.len() && chars[nx].is_ascii_digit() {
digit_str.push(chars[nx]); nx += 1; } let digit: usize = digit_str.parse().unwrap_or(0); let value = if digit == 0 {
vars_get("0").unwrap_or_default() } else {
arrays_get("@") .and_then(|a| a.get(digit.saturating_sub(1)).cloned()) .unwrap_or_default() }; let prefix: String = chars[..start_pos].iter().collect(); let suffix: String = chars[nx..].iter().collect(); let result = format!("{}{}{}", prefix, value, suffix); result_nodes.push(result.clone()); (result, prefix.len() + value.len(), result_nodes) } _ => {
let leading = chars.get(start_pos).copied().unwrap_or('\0');
if leading == Qstring || leading == Stringg {
let mut out = String::with_capacity(s.len());
out.push_str(&chars[..start_pos].iter().collect::<String>());
out.push('$');
out.push_str(&chars[start_pos + 1..].iter().collect::<String>());
result_nodes.push(out.clone());
(out, start_pos + 1, result_nodes)
} else {
result_nodes.push(s.to_string()); (s.to_string(), start_pos + 1, result_nodes) }
} } }
pub fn arithsubst(expr: &str, prefix: &str, rest: &str) -> String {
let expr = {
let bytes: Vec<char> = expr.chars().collect();
let mut out = String::with_capacity(expr.len());
let mut i = 0;
while i < bytes.len() {
let is_dollar = bytes[i] == '$' || bytes[i] == Stringg || bytes[i] == Qstring;
if is_dollar && i + 1 < bytes.len() && bytes[i + 1] == '#' {
let name_start = i + 2;
let mut name_end = name_start;
while name_end < bytes.len()
&& (bytes[name_end].is_ascii_alphanumeric() || bytes[name_end] == '_')
{
name_end += 1;
}
if name_end > name_start {
let name: String = bytes[name_start..name_end].iter().collect();
let count = if let Some(arr) = arrays_get(&name) {
arr.len()
} else if let Some(assoc) = assoc_get(&name) {
assoc.len()
} else if name == "@" || name == "*" {
arrays_get("@").map(|a| a.len()).unwrap_or(0)
} else if let Some(s) = vars_get(&name) {
s.chars().count()
} else {
0
};
out.push_str(&count.to_string());
i = name_end;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
out
};
let expanded = singsub(&expr);
let v = match crate::math::matheval(&expanded) {
Ok(n) => n,
Err(msg) => {
zerr(&msg);
crate::math::mnumber {
l: 0,
d: 0.0,
type_: MN_UNSET,
}
}
};
let outputradix = {
let r = crate::math::outputradix();
if r != 0 {
r } else {
vars_get("OUTPUT_RADIX")
.as_ref()
.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(0) }
};
let outputunderscore: i32 = crate::math::outputunderscore(); let b: String = if v.type_ == MN_UNSET {
"0".to_string() } else if (v.type_ == MN_FLOAT) && outputradix == 0 {
convfloat_underscore(v.d, outputunderscore)
} else {
let l = if (v.type_ == MN_FLOAT) {
v.d as i64
} else {
v.l
};
convbase_underscore(l, outputradix, outputunderscore)
};
format!("{}{}{}", prefix, b, rest) }
pub fn modify(s: &str, modifiers: &str) -> String {
let mut result = s.to_string(); let mut chars: std::iter::Peekable<std::str::Chars> = modifiers.chars().peekable();
while chars.peek() == Some(&':') {
chars.next();
let mut gbal = false; let mut wall = false; let mut sep: Option<String> = None; let mut fixed_point = false;
let mut max_iters: Option<u32> = None;
let mut any_flag_consumed = false;
loop {
match chars.peek() {
Some(&'g') => {
gbal = true; any_flag_consumed = true;
chars.next(); } Some(&'w') => {
wall = true; any_flag_consumed = true;
chars.next(); } Some(&'W') => {
any_flag_consumed = true;
chars.next(); if chars.peek() == Some(&':') {
chars.next(); let collected: String = chars.by_ref().take_while(|&c| c != ':').collect(); sep = Some(collected); } } Some(&'f') => {
fixed_point = true;
any_flag_consumed = true;
chars.next();
}
Some(&'F') => {
any_flag_consumed = true;
chars.next();
let mut num = String::new();
while let Some(&pc) = chars.peek() {
if pc.is_ascii_digit() {
num.push(pc);
chars.next();
} else {
break;
}
}
max_iters = num.parse().ok();
}
_ => break, } }
let modifier = match chars.next() {
Some(c) => c, None => {
if any_flag_consumed {
zerr("unrecognized modifier"); errflag_set_error();
}
break;
}
};
let mut count: i32 = 0; if matches!(modifier, 'h' | 't') {
let mut count_str = String::new(); while let Some(&pc) = chars.peek() {
if pc.is_ascii_digit() {
count_str.push(pc);
chars.next();
} else {
break;
}
}
if !count_str.is_empty() {
count = count_str.parse().unwrap_or(1); }
}
if modifier == 's' || modifier == 'S' {
let delim = match chars.next() {
Some(c) => c, None => break, };
let mut pat = String::new(); while let Some(&c) = chars.peek() {
if c == delim {
chars.next();
break;
}
if c == '\\' {
chars.next();
if let Some(&nx) = chars.peek() {
pat.push(nx);
chars.next();
}
} else {
pat.push(c);
chars.next();
}
}
let mut repl = String::new(); while let Some(&c) = chars.peek() {
if c == delim {
chars.next();
break;
}
if c == '\\' {
chars.next();
if let Some(&nx) = chars.peek() {
repl.push(nx);
chars.next();
}
} else if c == '&' {
chars.next();
repl.push_str(&pat);
} else {
repl.push(c);
chars.next();
}
}
let (eff_pat, anchor_head, anchor_tail) = if modifier == 'S' {
if let Some(rest) = pat.strip_prefix('#') {
(rest.to_string(), true, false) } else if let Some(rest) = pat.strip_suffix('%') {
(rest.to_string(), false, true) } else {
(pat.clone(), false, false) }
} else {
(pat.clone(), false, false) };
let use_glob = modifier == 'S' || isset(HISTSUBSTPATTERN);
let do_match = |hay: &str| -> Option<(usize, usize)> {
if use_glob {
let cv: Vec<char> = hay.chars().collect();
let n = cv.len();
for start in 0..=n {
for end in start..=n {
let span: String = cv[start..end].iter().collect();
if patcompile(&eff_pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &span))
{
let bs: usize = cv[..start].iter().map(|c| c.len_utf8()).sum();
let be: usize =
bs + cv[start..end].iter().map(|c| c.len_utf8()).sum::<usize>();
return Some((bs, be));
}
}
}
None
} else {
hay.find(eff_pat.as_str()).map(|s| (s, s + eff_pat.len()))
}
};
let __limit: u32 = max_iters.unwrap_or(if fixed_point { u32::MAX } else { 1 });
let mut __iters: u32 = 0;
loop {
let __prev_for_fp: Option<String> = if fixed_point || max_iters.is_some() {
Some(result.clone())
} else {
None
};
result = if anchor_head {
if use_glob {
let cv: Vec<char> = result.chars().collect();
let n = cv.len();
let mut found: Option<usize> = None;
for end in 0..=n {
let span: String = cv[..end].iter().collect();
if patcompile(&eff_pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &span))
{
found = Some(cv[..end].iter().map(|c| c.len_utf8()).sum());
break;
}
}
if let Some(be) = found {
format!("{}{}", repl, &result[be..])
} else {
result
}
} else if result.starts_with(&eff_pat) {
format!("{}{}", repl, &result[eff_pat.len()..]) } else {
result
} } else if anchor_tail {
if use_glob {
let cv: Vec<char> = result.chars().collect();
let n = cv.len();
let mut found: Option<usize> = None;
for start in 0..=n {
let span: String = cv[start..].iter().collect();
if patcompile(&eff_pat, PAT_HEAPDUP as i32, None)
.map_or(false, |__p| pattry(&__p, &span))
{
found = Some(cv[..start].iter().map(|c| c.len_utf8()).sum());
break;
}
}
if let Some(bs) = found {
format!("{}{}", &result[..bs], repl)
} else {
result
}
} else if result.ends_with(&eff_pat) {
format!("{}{}", &result[..result.len() - eff_pat.len()], repl)
} else {
result
} } else if gbal {
if use_glob {
let mut out = String::with_capacity(result.len());
let mut rem = result.as_str();
while let Some((s, e)) = do_match(rem) {
out.push_str(&rem[..s]);
out.push_str(&repl);
if e == s {
let mut chars = rem[s..].char_indices();
chars.next();
let next_s =
s + chars.next().map(|(b, _)| b).unwrap_or(rem.len() - s);
out.push_str(&rem[s..next_s]);
rem = &rem[next_s..];
} else {
rem = &rem[e..];
}
}
out.push_str(rem);
out
} else {
result.replace(eff_pat.as_str(), repl.as_str())
}
} else if use_glob {
if let Some((s, e)) = do_match(&result) {
format!("{}{}{}", &result[..s], repl, &result[e..])
} else {
result
}
} else {
result.replacen(eff_pat.as_str(), repl.as_str(), 1)
};
__iters += 1;
if __iters >= __limit {
break;
}
if let Some(p) = __prev_for_fp {
if result == p {
break;
}
} else {
break;
}
}
let mode: u8 = if modifier == 's' {
0
} else if anchor_head {
1
} else if anchor_tail {
2
} else {
3
};
*hsubl.lock().unwrap() = Some(eff_pat.clone()); *hsubr.lock().unwrap() = Some(repl.clone()); hsubpatopt.store(mode as i32, Ordering::Relaxed); if wall {
let separator = sep.as_deref().unwrap_or(" "); let words: Vec<&str> = result.split(separator).collect(); let modified: Vec<String> = words
.iter()
.map(|w| {
if gbal {
w.replace(pat.as_str(), repl.as_str())
}
else {
w.replacen(pat.as_str(), repl.as_str(), 1)
} })
.collect(); result = modified.join(separator); } continue; }
if modifier == '&' {
let last_subst = {
let p_opt = hsubl.lock().unwrap().clone();
let r_opt = hsubr.lock().unwrap().clone();
match (p_opt, r_opt) {
(Some(p), Some(r)) => {
let mode = hsubpatopt.load(Ordering::Relaxed) as u8;
Some((p, r, mode))
}
_ => None,
}
};
if let Some((p, r, mode)) = last_subst {
let apply = |w: &str| -> String {
match mode {
1 => {
if w.starts_with(p.as_str()) {
format!("{}{}", r, &w[p.len()..])
} else {
w.to_string()
}
}
2 => {
if w.ends_with(p.as_str()) {
format!("{}{}", &w[..w.len() - p.len()], r)
} else {
w.to_string()
}
}
_ => {
if gbal {
w.replace(p.as_str(), r.as_str())
} else {
w.replacen(p.as_str(), r.as_str(), 1)
}
}
}
};
if wall {
let separator = sep.as_deref().unwrap_or(" "); let words: Vec<&str> = result.split(separator).collect(); let modified: Vec<String> = words.iter().map(|w| apply(w)).collect();
result = modified.join(separator); } else {
result = apply(&result); } } continue; }
let dispatch = |w: &str| -> Option<String> {
match modifier {
'h' => Some(remtpath(w, count)), 't' => Some(remlpaths(w, count)), 'r' => Some(remtext(w)), 'e' => Some(rembutext(w)), 'l' => Some(casemodify(w, CASMOD_LOWER)), 'u' => Some(casemodify(w, CASMOD_UPPER)), 'q' => Some(quotestring(
w,
QT_BACKSLASH,
)),
'Q' => {
let mut out = String::with_capacity(w.len());
let mut chs = w.chars().peekable();
while let Some(c) = chs.next() {
if c == '\\' {
if let Some(nc) = chs.next() {
out.push(nc);
}
} else if c == '\'' || c == '"' {
} else {
out.push(c);
}
}
Some(out)
}
'a' => xsymlinks(w).ok(), 'A' | 'P' => {
let canon = std::fs::canonicalize(w)
.ok()
.map(|p| p.to_string_lossy().into_owned());
if let Some(c) = canon {
Some(c)
} else {
let mut p = std::path::PathBuf::from(w);
let mut tail: Vec<std::ffi::OsString> = Vec::new();
let resolved_prefix = loop {
if let Ok(rp) = std::fs::canonicalize(&p) {
break Some(rp);
}
match (
p.parent().map(|x| x.to_path_buf()),
p.file_name().map(|x| x.to_os_string()),
) {
(Some(parent), Some(file)) if !parent.as_os_str().is_empty() => {
tail.push(file);
p = parent;
}
_ => break None,
}
};
if let Some(mut rp) = resolved_prefix {
for t in tail.into_iter().rev() {
rp.push(t);
}
Some(rp.to_string_lossy().into_owned())
} else {
xsymlinks(w).ok()
}
}
}
'c' => {
if w.starts_with('/') || w.starts_with("./") || w.starts_with("../") {
Some(w.to_string()) } else if let Some(path) = getsparam("PATH") {
let mut found = None;
for dir in path.split(':') {
let p = std::path::PathBuf::from(dir).join(w);
if p.is_file() {
found = Some(p.to_string_lossy().into_owned());
break;
}
}
Some(found.unwrap_or_else(|| w.to_string()))
} else {
Some(w.to_string())
}
}
_ => None, }
};
if wall {
let separator = sep.as_deref().unwrap_or(" "); let words: Vec<&str> = result.split(separator).collect();
let mut modified: Vec<String> = Vec::with_capacity(words.len());
for w in &words {
match dispatch(w) {
Some(m) => modified.push(m),
None => {
zerr(&format!("unrecognized modifier `{}'", modifier));
errflag_set_error();
return String::new();
}
}
}
result = modified.join(separator);
} else {
match dispatch(&result) {
Some(m) => result = m,
None => {
zerr(&format!("unrecognized modifier `{}'", modifier));
errflag_set_error();
return String::new();
}
}
}
}
result }
pub fn dstackent(
ch: char,
val: i32,
dirstack: &[String],
pwd: &str,
pushdminus_set: bool,
) -> Option<String> {
let backwards = ch == if pushdminus_set { '+' } else { '-' };
let mut val = val; if !backwards && val == 0 {
return Some(pwd.to_string()); }
if !backwards {
val -= 1;
}
let n = dirstack.len() as i32; let idx = if backwards {
let i = n - val; if i < 0 {
return None;
} i as usize } else {
if val < 0 || val >= n {
return None;
} val as usize };
dirstack.get(idx).cloned() }
pub type LinkList = crate::ported::linklist::LinkList<String>;
const STRING: char = Stringg; const OUTANGPROC: char = OutangProc;
pub const NULSTRING: &str = "\u{a1}";
#[inline]
fn errflag_set() -> bool {
errflag.load(Ordering::Relaxed) != 0
}
#[inline]
fn errflag_set_error() {
errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::Relaxed);
}
thread_local! {
pub static IN_PARAMSUBST_NEST: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
}
thread_local! {
pub static SKIP_FILESUB: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
thread_local! {
pub static SUB_FLAGS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
}
fn vars_get(name: &str) -> Option<String> {
crate::ported::params::getsparam(name)
}
fn vars_contains(name: &str) -> bool {
paramtab()
.read()
.map_or(false, |tab| tab.contains_key(name))
|| std::env::var(name).is_ok()
}
fn arrays_get(name: &str) -> Option<Vec<String>> {
if name == "@" || name == "*" || name == "argv" {
let pp = crate::ported::builtin::PPARAMS.lock().ok()?;
return Some(pp.clone());
}
if name == "dirstack" {
if let Ok(d) = crate::ported::modules::parameter::DIRSTACK.lock() {
return Some(d.clone());
}
}
if name == "signals" {
return Some(crate::ported::jobs::sig_names_for_signals_param());
}
if name == "funcstack" {
if let Ok(f) = crate::ported::modules::parameter::FUNCSTACK.lock() {
return Some(f.iter().rev().map(|fs| fs.name.clone()).collect());
}
}
if name == "funcfiletrace" || name == "funcsourcetrace" || name == "functrace" {
if let Ok(f) = crate::ported::modules::parameter::FUNCSTACK.lock() {
return Some(
f.iter()
.rev()
.map(|fs| {
if name == "funcfiletrace" {
fs.filename.clone().unwrap_or_default()
} else {
format!("{}:{}", fs.name, fs.lineno)
}
})
.collect(),
);
}
}
let tab = paramtab().read().ok()?;
let pm = tab.get(name)?;
pm.u_arr.clone()
}
fn arrays_contains(name: &str) -> bool {
if name == "@" || name == "*" || name == "argv" {
return true;
}
if name == "dirstack" || name == "signals" {
return true;
}
let tied_partner = match name {
"path" => Some("PATH"),
"fpath" => Some("FPATH"),
"cdpath" => Some("CDPATH"),
"mailpath" => Some("MAILPATH"),
"manpath" => Some("MANPATH"),
"psvar" => Some("PSVAR"),
"module_path" => Some("MODULE_PATH"),
"zsh_eval_context" => Some("ZSH_EVAL_CONTEXT"),
"fignore" => Some("FIGNORE"),
_ => None,
};
if paramtab().read().map_or(false, |tab| {
tab.get(name).map_or(false, |pm| pm.u_arr.is_some())
}) {
return true;
}
if let Some(partner) = tied_partner {
let in_tab = paramtab()
.read()
.map_or(false, |tab| tab.contains_key(partner));
if in_tab || std::env::var(partner).is_ok() {
return true;
}
}
false
}
fn arrays_insert(name: String, value: Vec<String>) {
let mut tab = match paramtab().write() {
Ok(t) => t,
Err(_) => return,
};
if let Some(pm) = tab.get_mut(&name) {
pm.u_arr = Some(value);
pm.u_str = None;
pm.node.flags |= PM_ARRAY as i32;
} else {
let pm: Param = Box::new(param {
node: hashnode {
next: None,
nam: name.clone(),
flags: PM_ARRAY as i32,
},
u_data: 0,
u_arr: Some(value),
u_str: None,
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
});
tab.insert(name, pm);
}
}
fn assoc_get(name: &str) -> Option<indexmap::IndexMap<String, String>> {
paramtab_hashed_storage()
.lock()
.ok()
.and_then(|s| s.get(name).cloned())
}
fn assoc_contains(name: &str) -> bool {
paramtab_hashed_storage()
.lock()
.map_or(false, |s| s.contains_key(name))
}
fn exec_assignaparam(name: &str, parts: Vec<String>) {
arrays_insert(name.to_string(), parts);
}
#[cfg(test)] #[allow(non_snake_case)] mod tests {
use crate::zsh_h::{Hat, Tilde, CASMOD_CAPS};
use super::*;
#[test]
fn nulstring_matches_canonical_nularg_byte() {
let _g = crate::test_util::global_state_lock();
assert_eq!(Nularg as u32, 0xa1, "Src/zsh.h:206 — Nularg must be 0xa1");
assert_eq!(
NULSTRING, "\u{a1}",
"Src/subst.c:36 — NULSTRING must be the single Nularg sentinel char"
);
let chars: Vec<char> = NULSTRING.chars().collect();
assert_eq!(chars.len(), 1, "NULSTRING is a single char");
assert_eq!(chars[0], Nularg, "NULSTRING's single char IS Nularg");
}
#[test] fn test_getkeystring() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getkeystring("hello").0, "hello"); assert_eq!(getkeystring("hello\\nworld").0, "hello\nworld"); assert_eq!(getkeystring("\\t\\r\\n").0, "\t\r\n"); assert_eq!(getkeystring("\\x41").0, "A"); assert_eq!(getkeystring("\\u0041").0, "A"); }
#[test]
fn test_simple_param_expansion() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let name = "FOO".to_string();
let value = "bar".to_string();
setsparam(&name, &value);
let (result, _, _) = paramsubst("$FOO", 0, false, 0, &mut 0);
assert_eq!(result, "bar");
}
#[test] fn test_modify_head() {
let _g = crate::test_util::global_state_lock();
let result = modify("/path/to/file.txt", ":h"); assert_eq!(result, "/path/to"); }
#[test] fn test_modify_tail() {
let _g = crate::test_util::global_state_lock();
let result = modify("/path/to/file.txt", ":t"); assert_eq!(result, "file.txt"); }
#[test] fn test_modify_extension() {
let _g = crate::test_util::global_state_lock();
let result = modify("/path/to/file.txt", ":e"); assert_eq!(result, "txt"); }
#[test] fn test_modify_root() {
let _g = crate::test_util::global_state_lock();
let result = modify("/path/to/file.txt", ":r"); assert_eq!(result, "/path/to/file"); }
#[test] fn test_dopadding() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dopadding("hi", 5, 0, None, None, " ", " ", 0), " hi"); assert_eq!(dopadding("hi", 0, 5, None, None, " ", " ", 0), "hi "); let result = dopadding("hi", 3, 3, None, None, " ", " ", 0); assert!(result.len() >= 2, "result too short: {}", result); }
#[test] fn test_singsub() {
let _g = crate::test_util::global_state_lock();
let name = "X".to_string();
let value = "value".to_string();
setsparam(&name, &value); let result = singsub("X"); assert!(!result.is_empty() || result.is_empty()); }
#[test] fn casemodify_lower_uppercases_via_lowercase() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("Hello World", CASMOD_LOWER), "hello world"); assert_eq!(casemodify("MIXED-Case_42", CASMOD_LOWER), "mixed-case_42"); assert_eq!(casemodify("", CASMOD_LOWER), ""); }
#[test] fn casemodify_upper_uppercases_each_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("Hello World", CASMOD_UPPER), "HELLO WORLD"); assert_eq!(casemodify("ünicode", CASMOD_UPPER), "ÜNICODE"); assert_eq!(casemodify("", CASMOD_UPPER), ""); }
#[test] fn casemodify_caps_titlecases_each_word() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("hello world", CASMOD_CAPS), "Hello World"); assert_eq!(casemodify("FOO Bar", CASMOD_CAPS), "Foo Bar"); }
#[test] fn casemodify_caps_treats_punctuation_as_word_boundary() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("a-b c.d", CASMOD_CAPS), "A-B C.D"); assert_eq!(casemodify("foo_bar.baz", CASMOD_CAPS), "Foo_Bar.Baz"); }
#[test] fn remtpath_count_zero_strips_last_component() {
let _g = crate::test_util::global_state_lock();
assert_eq!(remtpath("/a/b/c", 0), "/a/b"); assert_eq!(remtpath("a/b/c", 0), "a/b"); assert_eq!(remtpath("foo", 0), "."); assert_eq!(remtpath("/foo", 0), "/"); assert_eq!(remtpath("/a/b/c/", 0), "/a/b"); assert_eq!(remtpath("/a/b//c//", 0), "/a/b"); }
#[test] fn remtpath_positive_count_keeps_n_components_from_front() {
let _g = crate::test_util::global_state_lock();
assert_eq!(remtpath("/a/b/c", 1), "/"); assert_eq!(remtpath("/a/b/c", 2), "/a"); assert_eq!(remtpath("/a/b/c", 3), "/a/b"); assert_eq!(remtpath("a/b/c", 1), "a"); assert_eq!(remtpath("a/b/c", 2), "a/b"); }
#[test] fn remtpath_root_is_always_root() {
let _g = crate::test_util::global_state_lock();
assert_eq!(remtpath("/", 0), "/"); assert_eq!(remtpath("///", 0), "/"); }
#[test] fn remlpaths_returns_last_n_components() {
let _g = crate::test_util::global_state_lock();
assert_eq!(remlpaths("/a/b/c", 1), "c"); assert_eq!(remlpaths("/a/b/c", 2), "b/c"); assert_eq!(remlpaths("/a/b/c", 3), "a/b/c"); assert_eq!(remlpaths("a/b/c", 1), "c"); assert_eq!(remlpaths("a/b/c", 2), "b/c"); }
#[test] fn remtext_strips_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(remtext("file.txt"), "file"); assert_eq!(remtext("/path/to/file.txt"), "/path/to/file"); assert_eq!(remtext("file.tar.gz"), "file.tar"); assert_eq!(remtext("noext"), "noext"); assert_eq!(remtext("/path.with.dot/noext"), "/path.with.dot/noext"); }
#[test] fn rembutext_keeps_only_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(rembutext("file.txt"), "txt"); assert_eq!(rembutext("/path/to/file.rs"), "rs"); assert_eq!(rembutext("file.tar.gz"), "gz"); assert_eq!(rembutext("noext"), ""); assert_eq!(rembutext("/path.with.dot/noext"), ""); }
#[test] fn chabspath_collapses_dot_and_dotdot() {
let _g = crate::test_util::global_state_lock();
assert_eq!(xsymlinks("/a/b/../c").unwrap(), "/a/c"); assert_eq!(xsymlinks("/a/./b/c").unwrap(), "/a/b/c"); assert_eq!(xsymlinks("/a/b/..").unwrap(), "/a"); }
#[test] fn getkeystring_decodes_basic_escapes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getkeystring("\\n").0, "\n"); assert_eq!(getkeystring("\\t").0, "\t"); assert_eq!(getkeystring("\\r").0, "\r"); assert_eq!(getkeystring("\\\\").0, "\\"); assert_eq!(getkeystring("plain").0, "plain"); }
#[test] fn getkeystring_decodes_hex_escape() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getkeystring("\\x41").0, "A"); assert_eq!(getkeystring("\\x7e").0, "~"); }
#[test] fn getkeystring_decodes_unicode_escape() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getkeystring("\\u00e9").0, "é"); assert_eq!(getkeystring("\\u4e2d").0, "中"); }
#[test] fn paramsubst_default_when_unset() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let (result, _, _) = paramsubst("${__default_unset_var:-fallback}", 0, false, 0, &mut 0); assert_eq!(result, "fallback"); }
#[test] fn paramsubst_assign_default_writes_indexed_array_slot() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let name = format!(
"__sub_arr_{}_{}",
module_path!().replace("::", "_"),
line!()
);
arrays_insert(name.clone(), Vec::new()); let pat = format!("${{{}[3]:=val}}", name);
let (_result, _, _) = paramsubst(&pat, 0, false, 0, &mut 0); let arr = arrays_get(&name).unwrap(); assert_eq!(arr.len(), 3); assert_eq!(arr[2], "val"); assert_eq!(arr[0], ""); assert_eq!(arr[1], ""); }
#[test] fn paramsubst_alternative_when_unset() {
let _g = crate::test_util::global_state_lock();
let (result, _, _) = paramsubst("${__alt_unset_var:+yes}", 0, false, 0, &mut 0); assert_eq!(result, ""); }
#[test]
fn get_strarg_extracts_paren_delimited_content() {
let _g = crate::test_util::global_state_lock();
let r = get_strarg("(foo)rest");
assert_eq!(
r,
Some(('(', "foo".to_string(), "rest")),
"(foo)rest must split into delim=( , content='foo', tail='rest'"
);
}
#[test]
fn get_strarg_empty_input_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(get_strarg(""), None);
}
#[test]
fn get_intarg_parses_paren_int() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let r = get_intarg("(42)rest");
assert_eq!(
r,
Some((42, "rest")),
"(42)rest must yield 42 + tail 'rest'"
);
}
#[test]
fn get_intarg_empty_input_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(get_intarg(""), None);
}
#[test]
fn strcatsub_concatenates_three_parts_plain() {
let _g = crate::test_util::global_state_lock();
assert_eq!(strcatsub("a", "b", "c", false), "abc");
assert_eq!(strcatsub("", "X", "", false), "X");
assert_eq!(strcatsub("[", "y", "]", false), "[y]");
}
#[test]
fn wcpadwidth_reports_one_for_ascii() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wcpadwidth('a', 0), 1);
assert_eq!(wcpadwidth('Z', 0), 1);
}
#[test]
fn wcpadwidth_reports_width_two_for_cjk_when_multi_set() {
let _g = crate::test_util::global_state_lock();
let w = wcpadwidth('中', 2);
assert!(w >= 1, "CJK char must have width >= 1 (got {w})");
}
#[test]
fn filesubstr_non_tilde_input_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(filesubstr("/literal/path", false), None);
assert_eq!(filesubstr("relative", false), None);
assert_eq!(filesubstr("", false), None);
}
#[test]
fn filesubstr_bare_tilde_resolves_to_home() {
let _g = crate::test_util::global_state_lock();
if let Ok(home) = std::env::var("HOME") {
let r = filesubstr("\u{98}", false);
assert_eq!(
r.as_deref(),
Some(home.as_str()),
"Tilde TOKEN `\\u{{98}}` must expand to $HOME"
);
}
}
#[test]
fn filesubstr_ascii_tilde_does_not_expand() {
let _g = crate::test_util::global_state_lock();
let r = filesubstr("~", false);
assert!(r.is_none(), "ASCII `~` must not tilde-expand (got {:?})", r);
}
#[test]
fn modify_h_strips_trailing_component() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("/foo/bar/baz", ":h"), "/foo/bar");
}
#[test]
fn modify_t_returns_trailing_component() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("/foo/bar/baz", ":t"), "baz");
}
#[test]
fn modify_r_strips_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("foo.txt", ":r"), "foo");
assert_eq!(modify("foo", ":r"), "foo", "no ext = no change");
}
#[test]
fn modify_e_returns_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("foo.txt", ":e"), "txt");
}
#[test]
fn modify_u_uppercases() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("hello", ":u"), "HELLO");
}
#[test]
fn modify_l_lowercases() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("HELLO", ":l"), "hello");
}
#[test]
fn modify_chained_modifiers_apply_left_to_right() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("foo.txt", ":r:u"), "FOO");
}
#[test]
fn modify_empty_modifier_returns_input_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(modify("foo/bar", ""), "foo/bar");
}
#[test]
fn check_colon_subscript_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(check_colon_subscript(""), None);
}
#[test]
fn check_colon_subscript_returns_none_on_modifier_letter() {
let _g = crate::test_util::global_state_lock();
assert_eq!(check_colon_subscript("h"), None);
assert_eq!(check_colon_subscript("t"), None);
assert_eq!(check_colon_subscript("r"), None);
assert_eq!(
check_colon_subscript("&5"),
None,
"`&` is the history-modifier prefix, not a subscript"
);
}
#[test]
fn check_colon_subscript_bare_colon_returns_zero() {
let _g = crate::test_util::global_state_lock();
let r = check_colon_subscript(":remainder");
assert!(r.is_some());
let (sub, _rest) = r.unwrap();
assert_eq!(sub, "0", "bare `:` subscript defaults to 0");
}
#[test]
fn quotesubst_passes_plain_ascii_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotesubst("hello world"), "hello world");
assert_eq!(quotesubst(""), "");
}
#[test]
fn singsub_passes_plain_text_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(singsub("hello"), "hello");
assert_eq!(singsub(""), "");
assert_eq!(singsub("no var"), "no var");
}
#[test]
fn token_byte_constants_match_zsh_h_canonical_values() {
let _g = crate::test_util::global_state_lock();
assert_eq!(Stringg as u32, 0x85, "Src/zsh.h:160 Stringg = $");
assert_eq!(Hat as u32, 0x86, "Src/zsh.h:161 Hat = ^");
assert_eq!(Inpar as u32, 0x88, "Src/zsh.h:163 Inpar = (");
assert_eq!(Outpar as u32, 0x8a, "Src/zsh.h:165 Outpar = )");
assert_eq!(Equals as u32, 0x8d, "Src/zsh.h:168 Equals = =");
assert_eq!(Inbrack as u32, 0x91, "Src/zsh.h:172 Inbrack = [");
assert_eq!(Outbrack as u32, 0x92, "Src/zsh.h:173 Outbrack = ]");
assert_eq!(Tick as u32, 0x93, "Src/zsh.h:174 Tick = `");
assert_eq!(Tilde as u32, 0x98, "Src/zsh.h:179 Tilde = ~");
assert_eq!(Snull as u32, 0x9d, "Src/zsh.h:193 Snull");
assert_eq!(Dnull as u32, 0x9e, "Src/zsh.h:194 Dnull");
assert_ne!(
Inpar as u32, 0x85,
"Inpar must NOT equal 0x85 (that's Stringg)"
);
assert_ne!(
Outpar as u32, 0x86,
"Outpar must NOT equal 0x86 (that's Hat)"
);
assert_ne!(
Equals as u32, 0x86,
"Equals must NOT equal 0x86 (that's Hat)"
);
assert_ne!(
Tick as u32, 0x83,
"Tick must NOT equal 0x83 (that's Meta lead byte)"
);
assert_ne!(
Snull as u32, 0x98,
"Snull must NOT equal 0x98 (that's Tilde)"
);
assert_ne!(
Dnull as u32, 0x97,
"Dnull must NOT equal 0x97 (that's Quest)"
);
}
#[test]
fn get_strarg_multibyte_content_safe() {
let _g = crate::test_util::global_state_lock();
let r = get_strarg(":foo:rest").unwrap();
assert_eq!(r.0, ':');
assert_eq!(r.1, "foo");
assert_eq!(r.2, "rest");
let r = get_strarg(":é:rest").unwrap();
assert_eq!(r.0, ':');
assert_eq!(r.1, "é", "c:1348 — multibyte content preserved verbatim");
assert_eq!(
r.2, "rest",
"c:1348 — rest starts AFTER close-delim (not mid-codepoint)"
);
let r = get_strarg("(héllo)tail").unwrap();
assert_eq!(r.0, '(');
assert_eq!(r.1, "héllo");
assert_eq!(r.2, "tail");
}
#[test]
fn get_strarg_unterminated_returns_consumed_content() {
let _g = crate::test_util::global_state_lock();
let r = get_strarg("(unclosed_content").unwrap();
assert_eq!(r.0, '(');
assert_eq!(r.1, "unclosed_content");
assert_eq!(r.2, "", "c:1348 — no close-delim → rest is empty");
}
fn psubst_one(name: &str, value: &str, expr: &str) -> String {
errflag.store(0, Ordering::Relaxed);
setsparam(name, value);
let needs_extglob = expr.contains("(#b)") || expr.contains("(#m)");
let saved_extglob = if needs_extglob {
let prev = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
Some(prev)
} else {
None
};
let out = if expr.contains("$((") {
singsub(expr)
} else {
let (out, _, _) = paramsubst(expr, 0, false, 0, &mut 0);
out
};
if let Some(prev) = saved_extglob {
crate::ported::options::opt_state_set("extendedglob", prev);
}
out
}
#[test]
fn paramsubst_colon_dash_keeps_nonempty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_F", "foo", "${PS_F:-bar}"), "foo");
}
#[test]
fn paramsubst_colon_dash_replaces_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_E", "", "${PS_E:-bar}"), "bar");
}
#[test]
fn paramsubst_dash_only_unset_check_keeps_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_E2", "", "${PS_E2-bar}"), "");
}
#[test]
fn paramsubst_colon_plus_uses_alt_when_set() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_AF", "foo", "${PS_AF:+alt}"), "alt");
}
#[test]
fn paramsubst_colon_plus_empty_when_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_AE", "", "${PS_AE:+alt}"), "");
}
#[test]
fn paramsubst_substring_first_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_H", "hello", "${PS_H:0:1}"), "h");
}
#[test]
fn paramsubst_substring_middle_three() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_H2", "hello", "${PS_H2:1:3}"), "ell");
}
#[test]
fn paramsubst_substring_offset_only() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_H3", "hello", "${PS_H3:2}"), "llo");
}
#[test]
fn paramsubst_substring_full_length() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_H4", "hello", "${PS_H4:0:5}"), "hello");
}
#[test]
fn paramsubst_length_of_5char_string() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_LH", "hello", "${#PS_LH}"), "5");
}
#[test]
fn paramsubst_length_of_empty_is_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_LE", "", "${#PS_LE}"), "0");
}
#[test]
fn paramsubst_strip_shortest_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_P", "/path/to/file.txt.bak", "${PS_P#*/}"),
"path/to/file.txt.bak"
);
}
#[test]
fn paramsubst_strip_longest_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_P2", "/path/to/file.txt.bak", "${PS_P2##*/}"),
"file.txt.bak"
);
}
#[test]
fn paramsubst_strip_literal_suffix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_PS", "/path/to/file.txt.bak", "${PS_PS%.bak}"),
"/path/to/file.txt"
);
}
#[test]
fn paramsubst_strip_shortest_suffix_glob() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_PSS", "/path/to/file.txt.bak", "${PS_PSS%.*}"),
"/path/to/file.txt"
);
}
#[test]
fn paramsubst_strip_longest_suffix_glob() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_PSL", "/path/to/file.txt.bak", "${PS_PSL%%.*}"),
"/path/to/file"
);
}
#[test]
fn paramsubst_replace_first_match() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_S", "aXbXc", "${PS_S/X/_}"), "a_bXc");
}
#[test]
fn paramsubst_replace_all_matches() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_S2", "aXbXc", "${PS_S2//X/_}"), "a_b_c");
}
#[test]
fn paramsubst_replace_anchored_start() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_S3", "aXbXc", "${PS_S3/#a/Z}"), "ZXbXc");
}
#[test]
fn paramsubst_replace_anchored_end() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_S4", "aXbXc", "${PS_S4/%c/Z}"), "aXbXZ");
}
#[test]
fn paramsubst_flag_L_lowercases() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_MIX1", "aBcDeF", "${(L)PS_MIX1}"), "abcdef");
}
#[test]
fn paramsubst_flag_U_uppercases() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_MIX2", "aBcDeF", "${(U)PS_MIX2}"), "ABCDEF");
}
#[test]
fn paramsubst_flag_C_capitalizes_first_char_only() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_MIX3", "aBcDeF", "${(C)PS_MIX3}"), "Abcdef");
}
#[test]
fn paramsubst_modifier_head() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_MH", "/path/to/file.txt.bak", "${PS_MH:h}"),
"/path/to"
);
}
#[test]
fn paramsubst_modifier_tail() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_MT", "/path/to/file.txt.bak", "${PS_MT:t}"),
"file.txt.bak"
);
}
#[test]
fn paramsubst_modifier_root_strips_one_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_MR", "/path/to/file.txt.bak", "${PS_MR:r}"),
"/path/to/file.txt"
);
}
#[test]
fn paramsubst_modifier_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_ME", "/path/to/file.txt.bak", "${PS_ME:e}"),
"bak"
);
}
#[test]
fn paramsubst_braced_bare_equals_unbraced_bare() {
let _g = crate::test_util::global_state_lock();
setsparam("PS_BB", "value");
let (a, _, _) = paramsubst("$PS_BB", 0, false, 0, &mut 0);
let (b, _, _) = paramsubst("${PS_BB}", 0, false, 0, &mut 0);
assert_eq!(a, b);
assert_eq!(a, "value");
}
#[test]
fn paramsubst_substring_negative_offset() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_NEG", "hello", "${PS_NEG:(-2)}"), "lo");
}
#[test]
fn paramsubst_substring_negative_offset_with_length() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_NEG2", "hello", "${PS_NEG2:(-3):2}"), "ll");
}
#[test]
fn paramsubst_substring_negative_length() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_NL", "hello", "${PS_NL:0:-1}"), "hell");
}
#[test]
fn paramsubst_flag_q_backslash_escapes_whitespace() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PS_Q", "hi there", "${(q)PS_Q}"), r"hi\ there");
}
#[test]
fn paramsubst_flag_qdash_uses_single_quotes_when_needed() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_QD", "hi there", "${(q-)PS_QD}"),
"'hi there'"
);
}
#[test]
fn paramsubst_modifier_gs_replaces_all() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PS_GS", "aXbXcXd", "${PS_GS:gs/X/_/}"),
"a_b_c_d"
);
}
#[test]
fn paramsubst_flag_P_dereferences_indirect_name() {
let _g = crate::test_util::global_state_lock();
setsparam("PSU_TARGET", "real_value");
let (out, _, _) = paramsubst("${(P)PSU_REF}", 0, false, 0, &mut 0);
setsparam("PSU_REF", "PSU_TARGET");
let (out2, _, _) = paramsubst("${(P)PSU_REF}", 0, false, 0, &mut 0);
let _ = out;
assert_eq!(out2, "real_value");
}
fn psubst_arr(name: &str, elements: &[&str], expr: &str) -> (String, Vec<String>) {
errflag.store(0, Ordering::Relaxed);
let _ = crate::ported::params::setaparam(
name,
elements.iter().map(|s| (*s).to_string()).collect(),
);
let (out, _, multi) = paramsubst(expr, 0, false, 0, &mut 0);
(out, multi)
}
#[test]
fn paramsubst_arr_index_one_returns_first_element() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA1", &["alpha", "beta", "gamma", "delta"], "${PSA1[1]}");
assert_eq!(out, "alpha");
}
#[test]
fn paramsubst_arr_index_two_returns_second_element() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA2", &["alpha", "beta", "gamma", "delta"], "${PSA2[2]}");
assert_eq!(out, "beta");
}
#[test]
fn paramsubst_arr_index_negative_one_returns_last_element() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA3", &["alpha", "beta", "gamma", "delta"], "${PSA3[-1]}");
assert_eq!(out, "delta");
}
#[test]
fn paramsubst_arr_index_negative_two_returns_second_to_last() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA4", &["alpha", "beta", "gamma", "delta"], "${PSA4[-2]}");
assert_eq!(out, "gamma");
}
#[test]
fn paramsubst_arr_index_out_of_range_returns_empty() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA5", &["alpha", "beta", "gamma", "delta"], "${PSA5[99]}");
assert_eq!(out, "");
}
#[test]
fn paramsubst_arr_length_returns_element_count() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA6", &["alpha", "beta", "gamma", "delta"], "${#PSA6}");
assert_eq!(out, "4");
}
#[test]
fn paramsubst_arr_length_three_elements() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA7", &["x", "y", "z"], "${#PSA7}");
assert_eq!(out, "3");
}
#[test]
fn paramsubst_arr_length_empty_array_is_zero() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA8", &[], "${#PSA8}");
assert_eq!(out, "0");
}
#[test]
fn paramsubst_arr_slice_one_two_returns_first_two_elements() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSA9", &["alpha", "beta", "gamma", "delta"], "${PSA9[1,2]}");
if !multi.is_empty() {
assert_eq!(multi, vec!["alpha", "beta"]);
} else {
assert_eq!(out, "alpha beta");
}
}
#[test]
fn paramsubst_arr_slice_two_to_end() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr(
"PSA10",
&["alpha", "beta", "gamma", "delta"],
"${PSA10[2,-1]}",
);
if !multi.is_empty() {
assert_eq!(multi, vec!["beta", "gamma", "delta"]);
} else {
assert_eq!(out, "beta gamma delta");
}
}
#[test]
fn paramsubst_arr_join_underscore_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr(
"PSA11",
&["alpha", "beta", "gamma", "delta"],
"${(j/_/)PSA11}",
);
assert_eq!(
out, "alpha_beta_gamma_delta",
"zsh 5.9 reference: print -r -- \"${{(j/_/)arr}}\" → alpha_beta_gamma_delta"
);
}
#[test]
fn paramsubst_arr_join_empty_string_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr(
"PSA12",
&["alpha", "beta", "gamma", "delta"],
"${(j::)PSA12}",
);
assert_eq!(
out, "alphabetagammadelta",
"zsh 5.9 reference: print -r -- \"${{(j::)arr}}\" → alphabetagammadelta"
);
}
#[test]
fn paramsubst_arr_F_flag_joins_with_newlines_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSA13", &["alpha", "beta", "gamma", "delta"], "${(F)PSA13}");
assert_eq!(
out, "alpha\nbeta\ngamma\ndelta",
"zsh 5.9 reference: print -r -- \"${{(F)arr}}\" → alpha\\nbeta\\ngamma\\ndelta"
);
}
#[test]
fn paramsubst_scalar_split_on_colon_yields_four_parts() {
let _g = crate::test_util::global_state_lock();
setsparam("PSA14", "a:b:c:d");
let (out, _, multi) = paramsubst("${(s/:/)PSA14}", 0, false, 0, &mut 0);
if !multi.is_empty() {
assert_eq!(multi, vec!["a", "b", "c", "d"]);
} else {
assert_eq!(out, "a b c d");
}
}
#[test]
fn paramsubst_arr_sort_ascending() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSA15", &["charlie", "alpha", "bravo"], "${(o)PSA15}");
if !multi.is_empty() {
assert_eq!(multi, vec!["alpha", "bravo", "charlie"]);
} else {
assert_eq!(out, "alpha bravo charlie");
}
}
#[test]
fn paramsubst_arr_sort_descending() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSA16", &["charlie", "alpha", "bravo"], "${(O)PSA16}");
if !multi.is_empty() {
assert_eq!(multi, vec!["charlie", "bravo", "alpha"]);
} else {
assert_eq!(out, "charlie bravo alpha");
}
}
#[test]
fn paramsubst_arr_filter_hash_removes_matching_literal_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSD1", &["foo", "bar", "baz", "qux"], "${PSD1[@]:#bar}");
if !multi.is_empty() {
assert_eq!(multi, vec!["foo", "baz", "qux"]);
} else {
assert_eq!(out, "foo baz qux", "zsh: ${{mix[@]:#bar}} → 'foo baz qux'");
}
}
#[test]
fn paramsubst_arr_filter_hash_removes_matching_glob_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSD2", &["foo", "bar", "baz", "qux"], "${PSD2[@]:#ba*}");
if !multi.is_empty() {
assert_eq!(multi, vec!["foo", "qux"]);
} else {
assert_eq!(out, "foo qux", "zsh: ${{mix[@]:#ba*}} → 'foo qux'");
}
}
#[test]
fn paramsubst_arr_per_element_suffix_strip() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSD3", &["foo.txt", "bar.txt", "baz.md"], "${PSD3[@]%.txt}");
if !multi.is_empty() {
assert_eq!(multi, vec!["foo", "bar", "baz.md"]);
} else {
assert_eq!(out, "foo bar baz.md");
}
}
#[test]
fn paramsubst_arr_per_element_basename_via_longest_prefix() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PSD4", &["/a/x", "/b/y", "/c/z"], "${PSD4[@]##*/}");
if !multi.is_empty() {
assert_eq!(multi, vec!["x", "y", "z"]);
} else {
assert_eq!(out, "x y z");
}
}
#[test]
fn paramsubst_arr_length_of_indexed_element_is_string_length_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSD5", &["hello", "world"], "${#PSD5[1]}");
assert_eq!(out, "5", "zsh: ${{#arr[1]}} → string-length of first elem");
}
#[test]
fn paramsubst_arr_substring_of_indexed_element_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSD6", &["hello", "world"], "${PSD6[1]:0:3}");
assert_eq!(out, "hel", "zsh: ${{arr[1]:0:3}} → first 3 chars of arr[1]");
}
#[test]
fn paramsubst_arr_sort_scalar_context_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let _ = crate::ported::params::setaparam(
"PSD7",
["charlie", "alpha", "bravo"]
.iter()
.map(|s| (*s).to_string())
.collect(),
);
let (out, _, _) = paramsubst("${(o)PSD7}", 0, true, 0, &mut 0);
assert_eq!(
out, "charlie alpha bravo",
"zsh: scalar (DQ) context (o) does NOT sort; got {out:?}"
);
}
#[test]
fn paramsubst_arr_compound_oj_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSD8", &["charlie", "alpha", "bravo"], "${(oj/-/)PSD8}");
assert_eq!(
out, "charlie-alpha-bravo",
"zsh: (oj/-/) does NOT sort in scalar context; got {out:?}"
);
}
#[test]
fn paramsubst_arr_compound_Fo_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSD9", &["charlie", "alpha", "bravo"], "${(Fo)PSD9}");
assert_eq!(
out, "charlie\nalpha\nbravo",
"zsh: (Fo) → newline-join unsorted; got {out:?}"
);
}
#[test]
fn paramsubst_arr_join_on_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PSD10", &[], "${(j/,/)PSD10}");
assert_eq!(out, "");
}
#[test]
fn paramsubst_unset_param_uses_default() {
let _g = crate::test_util::global_state_lock();
crate::ported::params::unsetparam("PSD_UNSET");
let (out, _, _) = paramsubst("${PSD_UNSET:-fallback}", 0, false, 0, &mut 0);
assert_eq!(out, "fallback");
}
#[test]
fn paramsubst_flag_qdash_on_empty_string_emits_empty_quotes_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let out = psubst_one("PSD_QE", "", "${(q-)PSD_QE}");
assert_eq!(out, "''", "zsh: ${{(q-)empty}} → '' (empty quotes)");
}
#[test]
fn paramsubst_flag_qplus_picks_shortest_quoting_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
crate::ported::utils::inittyptab();
let out = psubst_one("PSD_QP", "hi there", "${(q+)PSD_QP}");
assert_eq!(
out, "'hi there'",
"zsh: ${{(q+)\"hi there\"}} → 'hi there' (shortest valid quoting)"
);
}
const PATH_FIXTURE: &str = "/path/to/file.txt.bak";
#[test]
fn paramsubst_mod_chain_h_t_returns_last_dir_component() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PMC1", PATH_FIXTURE, "${PMC1:h:t}"), "to");
}
#[test]
fn paramsubst_mod_chain_t_r_basename_strip_last_ext() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PMC2", PATH_FIXTURE, "${PMC2:t:r}"), "file.txt");
}
#[test]
fn paramsubst_mod_chain_r_t_strip_then_basename() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PMC3", PATH_FIXTURE, "${PMC3:r:t}"), "file.txt");
}
#[test]
fn paramsubst_mod_chain_r_e_returns_inner_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PMC4", PATH_FIXTURE, "${PMC4:r:e}"), "txt");
}
#[test]
fn paramsubst_mod_chain_r_r_strips_two_extensions() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PMC5", PATH_FIXTURE, "${PMC5:r:r}"),
"/path/to/file"
);
}
#[test]
fn paramsubst_mod_chain_t_e_basename_then_extension() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PMC6", PATH_FIXTURE, "${PMC6:t:e}"), "bak");
}
#[test]
fn paramsubst_mod_chain_s_single_substitution() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PMC7", PATH_FIXTURE, "${PMC7:s/file/X/}"),
"/path/to/X.txt.bak"
);
}
#[test]
fn paramsubst_mod_chain_gs_global_substitution() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PMC8", PATH_FIXTURE, "${PMC8:gs/t/Z/}"),
"/paZh/Zo/file.ZxZ.bak"
);
}
#[test]
fn paramsubst_mod_chain_s_then_r_subst_then_strip() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PMC9", PATH_FIXTURE, "${PMC9:s/file/X/:r}"),
"/path/to/X.txt"
);
}
#[test]
fn paramsubst_mod_chain_r_then_s_strip_then_subst() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PMC10", PATH_FIXTURE, "${PMC10:r:s/file/X/}"),
"/path/to/X.txt"
);
}
#[test]
fn paramsubst_mod_chain_t_then_gs_basename_then_gsub() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PMC11", PATH_FIXTURE, "${PMC11:t:gs/./_/}"),
"file_txt_bak"
);
}
#[test]
fn paramsubst_mod_q_quotes_backslash_escape() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PMC12", "hi there", "${PMC12:q}"), r"hi\ there");
}
fn build_test_hash(name: &str) {
errflag.store(0, Ordering::Relaxed);
crate::ported::params::unsetparam(name);
let _ = crate::ported::params::sethparam(
name,
vec![
"k1".into(),
"v1".into(),
"k2".into(),
"v2".into(),
"k3".into(),
"v3".into(),
],
);
}
#[test]
fn paramsubst_hash_indexed_returns_value() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH1");
let (out, _, _) = paramsubst("${PSH1[k1]}", 0, false, 0, &mut 0);
assert_eq!(out, "v1");
crate::ported::params::unsetparam("PSH1");
}
#[test]
fn paramsubst_hash_indexed_second_key() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH2");
let (out, _, _) = paramsubst("${PSH2[k2]}", 0, false, 0, &mut 0);
assert_eq!(out, "v2");
crate::ported::params::unsetparam("PSH2");
}
#[test]
fn paramsubst_hash_missing_key_returns_empty() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH3");
let (out, _, _) = paramsubst("${PSH3[missing]}", 0, false, 0, &mut 0);
assert_eq!(out, "");
crate::ported::params::unsetparam("PSH3");
}
#[test]
fn paramsubst_hash_length_returns_element_count() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH4");
let (out, _, _) = paramsubst("${#PSH4}", 0, false, 0, &mut 0);
assert_eq!(out, "3", "3 key-value pairs → length 3");
crate::ported::params::unsetparam("PSH4");
}
#[test]
fn paramsubst_hash_k_flag_returns_keys() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH5");
let (out, _, multi) = paramsubst("${(k)PSH5}", 0, false, 0, &mut 0);
let mut keys: Vec<String> = if !multi.is_empty() {
multi
} else {
out.split_whitespace().map(|s| s.to_string()).collect()
};
keys.sort();
assert_eq!(keys, vec!["k1", "k2", "k3"]);
crate::ported::params::unsetparam("PSH5");
}
#[test]
fn paramsubst_hash_v_flag_returns_values() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH6");
let (out, _, multi) = paramsubst("${(v)PSH6}", 0, false, 0, &mut 0);
let mut vals: Vec<String> = if !multi.is_empty() {
multi
} else {
out.split_whitespace().map(|s| s.to_string()).collect()
};
vals.sort();
assert_eq!(vals, vec!["v1", "v2", "v3"]);
crate::ported::params::unsetparam("PSH6");
}
#[test]
fn paramsubst_hash_kv_flag_returns_alternating_pairs() {
let _g = crate::test_util::global_state_lock();
build_test_hash("PSH7");
let (out, _, multi) = paramsubst("${(kv)PSH7}", 0, false, 0, &mut 0);
let mut all: Vec<String> = if !multi.is_empty() {
multi
} else {
out.split_whitespace().map(|s| s.to_string()).collect()
};
all.sort();
assert_eq!(
all,
vec!["k1", "k2", "k3", "v1", "v2", "v3"],
"kv must produce all keys + all values (6 entries total)"
);
crate::ported::params::unsetparam("PSH7");
}
#[test]
fn paramsubst_arr_slice_to_negative_two() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PS_S1", &["a", "b", "c", "d", "e"], "${PS_S1[1,-2]}");
if !multi.is_empty() {
assert_eq!(multi, vec!["a", "b", "c", "d"]);
} else {
assert_eq!(out, "a b c d");
}
}
#[test]
fn paramsubst_arr_slice_negative_three_to_negative_one() {
let _g = crate::test_util::global_state_lock();
let (out, multi) = psubst_arr("PS_S2", &["a", "b", "c", "d", "e"], "${PS_S2[-3,-1]}");
if !multi.is_empty() {
assert_eq!(multi, vec!["c", "d", "e"]);
} else {
assert_eq!(out, "c d e");
}
}
#[test]
fn paramsubst_arr_paren_negative_one_is_last_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PS_S3", &["a", "b", "c", "d", "e"], "${PS_S3[(-1)]}");
assert_eq!(out, "e", "zsh: arr=(a b c d e); ${{arr[(-1)]}} → e");
}
#[test]
fn paramsubst_arr_paren_positive_one_is_first_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let (out, _) = psubst_arr("PS_S4", &["a", "b", "c", "d", "e"], "${PS_S4[(1)]}");
assert_eq!(out, "a", "zsh: arr=(a b c d e); ${{arr[(1)]}} → a");
}
#[test]
fn paramsubst_arr_set_intersection_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let _ = crate::ported::params::setaparam("PS_OTHER1", vec!["b".into(), "d".into()]);
let (out, multi) = psubst_arr(
"PS_LIST1",
&["a", "b", "c", "d", "e"],
"${PS_LIST1[@]:*PS_OTHER1}",
);
if !multi.is_empty() {
assert_eq!(multi, vec!["b", "d"]);
} else {
assert_eq!(out, "b d");
}
}
#[test]
fn paramsubst_arr_set_subtraction_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
let _ = crate::ported::params::setaparam("PS_OTHER2", vec!["b".into(), "d".into()]);
let (out, multi) = psubst_arr(
"PS_LIST2",
&["a", "b", "c", "d", "e"],
"${PS_LIST2[@]:|PS_OTHER2}",
);
if !multi.is_empty() {
assert_eq!(multi, vec!["a", "c", "e"]);
} else {
assert_eq!(out, "a c e");
}
}
#[test]
fn paramsubst_colon_equals_assigns_and_returns_default_on_unset() {
let _g = crate::test_util::global_state_lock();
crate::ported::params::unsetparam("PS_ASSIGN1");
errflag.store(0, Ordering::Relaxed);
let (out, _, _) = paramsubst("${PS_ASSIGN1:=newval}", 0, false, 0, &mut 0);
assert_eq!(out, "newval", "expansion returns the assigned value");
assert_eq!(
crate::ported::params::getsparam("PS_ASSIGN1").as_deref(),
Some("newval"),
"X must be SET to newval by `:=` operator"
);
crate::ported::params::unsetparam("PS_ASSIGN1");
}
#[test]
fn paramsubst_colon_equals_assigns_on_empty_too() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
setsparam("PS_ASSIGN2", "");
let (out, _, _) = paramsubst("${PS_ASSIGN2:=newval}", 0, false, 0, &mut 0);
assert_eq!(out, "newval");
assert_eq!(
crate::ported::params::getsparam("PS_ASSIGN2").as_deref(),
Some("newval"),
"empty X gets reassigned to newval"
);
crate::ported::params::unsetparam("PS_ASSIGN2");
}
#[test]
fn paramsubst_colon_equals_noop_on_nonempty() {
let _g = crate::test_util::global_state_lock();
errflag.store(0, Ordering::Relaxed);
setsparam("PS_ASSIGN3", "preserved");
let (out, _, _) = paramsubst("${PS_ASSIGN3:=newval}", 0, false, 0, &mut 0);
assert_eq!(out, "preserved");
assert_eq!(
crate::ported::params::getsparam("PS_ASSIGN3").as_deref(),
Some("preserved"),
"non-empty X stays unchanged"
);
crate::ported::params::unsetparam("PS_ASSIGN3");
}
#[test]
fn paramsubst_compound_uppercase_then_substring() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PCC1", "Hello", "${(U)PCC1:0:3}"), "HEL");
}
#[test]
fn paramsubst_compound_uppercase_then_drop_first() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PCC2", "Hello", "${(U)PCC2:1}"), "ELLO");
}
#[test]
fn paramsubst_compound_length_of_uppercase() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PCC3", "Hello", "${#${(U)PCC3}}"), "5");
}
#[test]
fn paramsubst_compound_lowercase_strip_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PCC4", "FOOBARBAZ", "${(L)PCC4##*B}"),
"az",
"zsh: ${{(L)FOOBARBAZ##*B}} → 'az' (strip + lower)"
);
}
#[test]
fn paramsubst_compound_length_of_strip_result() {
let _g = crate::test_util::global_state_lock();
assert_eq!(psubst_one("PCC5", "FOOBARBAZ", "${#${PCC5##*B}}"), "2");
}
#[test]
fn paramsubst_nested_double_strip_prefix_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PCC6", "alpha:beta:gamma", "${${PCC6#*:}#*:}"),
"gamma",
"zsh: alpha:beta:gamma → beta:gamma → gamma"
);
}
#[test]
fn paramsubst_nested_double_strip_suffix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
psubst_one("PCC7", "alpha:beta:gamma", "${${PCC7%:*}%:*}"),
"alpha"
);
}
#[test]
fn paramsubst_strip_pound_b_backref_keeps_strip_semantic() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let result = psubst_one(
"ZP_BB1",
"look for a match in here",
"${ZP_BB1%%(#b)(match)*}",
);
crate::ported::options::opt_state_set("extendedglob", saved);
assert_eq!(
result, "look for a ",
"Test/D04parameter.ztst:1250 — (#b) doesn't change strip result"
);
}
#[test]
fn paramsubst_pound_b_backref_populates_match_array() {
let _g = crate::test_util::global_state_lock();
let _ = psubst_one(
"ZP_BB2",
"look for a match in here",
"${ZP_BB2%%(#b)(match)*}",
);
let m = crate::ported::params::getaparam("match");
assert_eq!(
m.as_deref(),
Some(&["match".to_string()][..]),
"Test/D04parameter.ztst:1251 — $match[1]=match",
);
}
#[test]
fn paramsubst_pound_b_populates_mbegin_mend() {
let _g = crate::test_util::global_state_lock();
let _ = psubst_one(
"ZP_BB3",
"look for a match in here",
"${ZP_BB3%%(#b)(match)*}",
);
let b = crate::ported::params::getaparam("mbegin");
let e = crate::ported::params::getaparam("mend");
assert_eq!(
b.as_deref(),
Some(&["12".to_string()][..]),
"Test/D04parameter.ztst:1251 — $mbegin[1]=12"
);
assert_eq!(
e.as_deref(),
Some(&["16".to_string()][..]),
"Test/D04parameter.ztst:1251 — $mend[1]=16"
);
}
#[test]
fn paramsubst_pound_m_flag_strip_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_MM1",
"and look for a MATCH in here",
"${(S)ZP_MM1%%(#m)M*H}",
);
assert_eq!(
result, "and look for a in here",
"Test/D04parameter.ztst:1262 — (S) + (#m) substring strip",
);
let m = crate::ported::params::getsparam("MATCH");
assert_eq!(
m.as_deref(),
Some("MATCH"),
"Test/D04parameter.ztst:1263 — $MATCH=MATCH"
);
}
#[test]
fn paramsubst_pound_m_substitution_with_match_refs() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_MM2",
"this is a string",
"${ZP_MM2//(#m)s/$MATCH $MBEGIN $MEND}",
);
assert_eq!(
result, "this 4 4 is 7 7 a s 11 11tring",
"Test/D04parameter.ztst:1275 — (#m) in subst with $MATCH/$MBEGIN/$MEND",
);
}
#[test]
fn paramsubst_pound_b_nested_capture_transform() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_BBN",
"aleftkept",
"${ZP_BBN//(#b)(*)left/${match[1]/a/andsome}}",
);
assert_eq!(
result, "andsomekept",
"Test/D04parameter.ztst:1307,1310 — (#b)+nested subst on capture",
);
}
#[test]
fn paramsubst_pound_b_fully_anchored_must_scan_whole_string() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_BBA", "string", "${(S)ZP_BBA//#%((#b)(*))/different}");
assert_eq!(
result, "different",
"Test/D04parameter.ztst:2358 — full-anchor search expected",
);
}
#[test]
fn paramsubst_pound_m_with_end_anchor_in_nested_subst() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_MMN",
"abcdefghijklmnopqrstuvwxyz",
"${${ZP_MMN%[aeiou]*}/(#m)?(#e)/${(U)MATCH}}",
);
assert_eq!(
result, "abcdefghijklmnopqrsT",
"Test/D04parameter.ztst:893 — nested (#m)+(#e)+(U)MATCH",
);
}
#[test]
fn paramsubst_zsh_corpus_strip_shortest_prefix() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_S1", "This is very boring indeed.", "${ZP_S1#*s}");
assert_eq!(
result, " is very boring indeed.",
"Test/D04parameter.ztst:129 — ${{var#*s}} shortest prefix to first 's'",
);
}
#[test]
fn paramsubst_zsh_corpus_strip_longest_prefix() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_S2", "This is very boring indeed.", "${ZP_S2##*s}");
assert_eq!(
result, " very boring indeed.",
"Test/D04parameter.ztst:130 — ${{var##*s}} longest prefix",
);
}
#[test]
fn paramsubst_zsh_corpus_colon_hash_filter_scalar_match() {
let _g = crate::test_util::global_state_lock();
let r1 = psubst_one("ZP_CH1", "does match", "${ZP_CH1:#does * match}");
assert_eq!(r1, "does match", "ztst:145 — non-match yields self");
let r2 = psubst_one("ZP_CH2", "does not match", "${ZP_CH2:#does * match}");
assert_eq!(r2, "", "ztst:146 — match yields empty");
}
#[test]
fn paramsubst_zsh_corpus_scalar_single_replace_longest_leftmost() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_R1",
"arthur boldly claws dogs every fight",
"${ZP_R1/[aeiou]*g/a braw bricht moonlicht nicht the nic}",
);
assert_eq!(
result, "a braw bricht moonlicht nicht the nicht",
"ztst:159 — leftmost-longest [aeiou]*g replace",
);
}
#[test]
fn paramsubst_zsh_corpus_substring_mode_shortest_replace() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_R2",
"arthur boldly claws dogs every fight",
"${(S)ZP_R2/[aeiou]*g/relishe}",
);
assert_eq!(
result, "relishes every fight",
"ztst:160 — (S) shortest-leftmost replace",
);
}
#[test]
fn paramsubst_zsh_corpus_global_replace_greedy_eats_rest() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_G1",
"o this is so, so so very dull",
"${ZP_G1//o*/Please no}",
);
assert_eq!(
result, "Please no",
"ztst:172 — greedy o* match consumes from first 'o' to end",
);
}
#[test]
fn paramsubst_zsh_corpus_global_replace_substring_mode_per_char() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_G2",
"o this is so, so so very dull",
"${(S)ZP_G2//o*/Please no}",
);
assert_eq!(
result, "Please no this is sPlease no, sPlease no sPlease no very dull",
"ztst:173 — (S) substring per-occurrence replace",
);
}
#[test]
fn paramsubst_zsh_corpus_replace_literal_backslash() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_BS", r"a\string\with\backslashes", r"${ZP_BS//\\/-}");
assert_eq!(
result, "a-string-with-backslashes",
"ztst:192 — global \\ → -",
);
}
#[test]
fn paramsubst_zsh_corpus_replace_escaped_slash() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_SL", "a/string/with/slashes", r"${ZP_SL//\//-}");
assert_eq!(
result, "a-string-with-slashes",
"ztst:194 — global escaped / → -",
);
}
#[test]
fn paramsubst_zsh_corpus_lowercase_flag() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_LC",
"yOU KNOW, THE ONE WITH wILLIAM dALRYMPLE",
"${(L)ZP_LC}",
);
assert_eq!(
result, "you know, the one with william dalrymple",
"ztst:415 — (L) lowercases all chars",
);
}
#[test]
fn paramsubst_zsh_corpus_uppercase_flag() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_UC", "doing that tour of India.", "${(U)ZP_UC}");
assert_eq!(
result, "DOING THAT TOUR OF INDIA.",
"ztst:416 — (U) uppercases all chars",
);
}
#[test]
fn paramsubst_zsh_corpus_capitalize_flag() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_CAP",
"instead here I am stuck by the computer",
"${(C)ZP_CAP}",
);
assert_eq!(
result, "Instead Here I Am Stuck By The Computer",
"ztst:421 — (C) Title Case",
);
}
#[test]
fn paramsubst_zsh_corpus_pound_m_with_tokenized_glob_input() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_TKM", "", "${${~:-*}//(#m)*/$MATCH=$MATCH}");
assert_eq!(result, "*=*", "ztst:1279 — tokenized * passed through (#m)",);
}
#[test]
fn paramsubst_zsh_corpus_pound_b_with_nested_global_subst_on_capture() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZP_BBG",
"aleftkept",
"${ZP_BBG//(#b)(*)left/${match//a/andsome}}",
);
assert_eq!(
result, "andsomekept",
"ztst:1310 — (#b) capture used in nested // subst",
);
}
#[test]
fn paramsubst_zsh_corpus_array_first_element() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr("ZA_F", &["a", "b", "c", "d", "e", "f", "g"], "${ZA_F[1]}");
assert_eq!(s, "a", "ztst:14 — 1-based first element");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_one_to_four() {
let _g = crate::test_util::global_state_lock();
let (_, v) = psubst_arr("ZA_S", &["a", "b", "c", "d", "e", "f", "g"], "${ZA_S[1,4]}");
assert_eq!(v.join(" "), "a b c d", "ztst:18 — [1,4] slice");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_empty_when_end_before_start() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr(
"ZA_E1",
&["a", "b", "c", "d", "e", "f", "g"],
"${ZA_E1[1,0]}",
);
assert_eq!(s, "", "ztst:22 — [1,0] empty");
}
#[test]
fn paramsubst_zsh_corpus_array_index_zero_is_empty() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr("ZA_Z", &["a", "b", "c", "d", "e", "f", "g"], "${ZA_Z[0]}");
assert_eq!(s, "", "ztst:34 — [0] empty in 1-based zsh");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_zero_to_zero_is_empty() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr(
"ZA_ZZ",
&["a", "b", "c", "d", "e", "f", "g"],
"${ZA_ZZ[0,0]}",
);
assert_eq!(s, "", "ztst:38 — [0,0] empty");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_zero_to_one_yields_first() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr(
"ZA_OZ",
&["a", "b", "c", "d", "e", "f", "g"],
"${ZA_OZ[0,1]}",
);
assert_eq!(s, "a", "ztst:42 — [0,1] yields first element");
}
#[test]
fn paramsubst_zsh_corpus_array_inner_element() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr("ZA_I", &["a", "b", "c", "d", "e", "f", "g"], "${ZA_I[3]}");
assert_eq!(s, "c", "ztst:46 — [3] returns third element");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_negative_end() {
let _g = crate::test_util::global_state_lock();
let (_, v) = psubst_arr(
"ZA_NE",
&["a", "b", "c", "d", "e", "f", "g"],
"${ZA_NE[2,-4]}",
);
assert_eq!(v.join(" "), "b c d", "ztst:54 — [2,-4] slice");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_negative_start() {
let _g = crate::test_util::global_state_lock();
let (_, v) = psubst_arr(
"ZA_NS",
&["a", "b", "c", "d", "e", "f", "g"],
"${ZA_NS[-4,5]}",
);
assert_eq!(v.join(" "), "d e", "ztst:58 — [-4,5] slice");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_both_negative() {
let _g = crate::test_util::global_state_lock();
let (_, v) = psubst_arr(
"ZA_NN",
&["a", "b", "c", "d", "e", "f", "g"],
"${ZA_NN[-6,-2]}",
);
assert_eq!(v.join(" "), "b c d e f", "ztst:62 — [-6,-2] slice");
}
#[test]
fn paramsubst_zsh_corpus_array_slice_reversed_indices_empty() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr("ZA_R", &["a", "b", "c", "d", "e", "f", "g"], "${ZA_R[4,1]}");
assert_eq!(s, "", "ztst:26 — [4,1] reversed empty");
}
#[test]
fn paramsubst_zsh_corpus_array_index_zero_no_ksh_zero() {
let _g = crate::test_util::global_state_lock();
let (s, _) = psubst_arr("ZA_KZ0", &["one", "two", "three", "four"], "X${ZA_KZ0[0]}X");
assert_eq!(
s, "XX",
"ztst:203 — array[0] empty without KSH_ZERO_SUBSCRIPT"
);
}
#[test]
fn paramsubst_zsh_corpus_string_subscript_zero_one_and_slice() {
let _g = crate::test_util::global_state_lock();
let s0 = psubst_one("ZS_W", "Why, if it isn't Officer Dibble", "${ZS_W[0]}");
let s1 = psubst_one("ZS_W", "Why, if it isn't Officer Dibble", "${ZS_W[1]}");
let s03 = psubst_one("ZS_W", "Why, if it isn't Officer Dibble", "${ZS_W[0,3]}");
assert_eq!(
format!("[{s0}][{s1}][{s03}]"),
"[][W][Why]",
"ztst:236 — string subscripts [0]/[1]/[0,3]",
);
}
#[test]
fn paramsubst_zsh_corpus_scalar_subscript_i_flag_first_match() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZS_T",
"Twinkle, twinkle, little *, [how] I [wonder] what? You are!",
"${ZS_T[(i)winkle]}",
);
assert_eq!(result, "2", "ztst:14 — (i) flag returns first index");
}
#[test]
fn paramsubst_zsh_corpus_scalar_subscript_i_no_match_returns_len_plus_one() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZS_NM",
"Twinkle, twinkle, little *, [how] I [wonder] what? You are!",
"${ZS_NM[(i)x]}",
);
assert_eq!(result, "61", "ztst:32 — (i) no-match returns len+1");
}
#[test]
fn paramsubst_zsh_corpus_hash_prefix_returns_length() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZL_L", "hello", "${#ZL_L}");
assert_eq!(result, "5", "${{#var}} returns char length");
}
#[test]
fn paramsubst_zsh_corpus_hash_prefix_empty_string_zero() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZL_E", "", "${#ZL_E}");
assert_eq!(result, "0", "${{#var}} empty returns 0");
}
#[test]
fn paramsubst_zsh_corpus_hash_prefix_multibyte_codepoints() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZL_M", "héllo", "${#ZL_M}");
assert_eq!(result, "5", "${{#var}} multibyte counts codepoints");
}
#[test]
fn paramsubst_zsh_corpus_l_flag_lowercases_scalar() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZL_C", "HELLO WORLD", "${(L)ZL_C}");
assert_eq!(result, "hello world", "(L) flag lowercases");
}
#[test]
fn paramsubst_zsh_corpus_u_flag_uppercases_scalar() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZU_C", "hello world", "${(U)ZU_C}");
assert_eq!(result, "HELLO WORLD", "(U) flag uppercases");
}
#[test]
fn paramsubst_zsh_corpus_c_flag_capitalizes_words() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZC_S", "hello world foo bar", "${(C)ZC_S}");
assert_eq!(result, "Hello World Foo Bar", "(C) flag capitalizes words");
}
#[test]
fn paramsubst_zsh_corpus_q_flag_dequotes_scalar() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one(
"ZQ_S",
r#"'and now' "even the pubs" \a\r\e shut."#,
"${(Q)ZQ_S}",
);
assert_eq!(
result, "and now even the pubs are shut.",
"ztst:467 — (Q) strips quotes + backslashes",
);
}
#[test]
fn paramsubst_zsh_corpus_q_minus_flag_no_quote_needed() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZQM_P", "foo", "${(q-)ZQM_P}");
assert_eq!(result, "foo", "ztst:458 — (q-) on plain word no quotes");
}
#[test]
fn paramsubst_zsh_corpus_q_minus_flag_space_gets_quoted() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZQM_SP", "foo bar", "${(q-)ZQM_SP}");
assert_eq!(
result, "'foo bar'",
"ztst:459 — (q-) quotes when space present"
);
}
#[test]
fn paramsubst_zsh_corpus_q_minus_flag_glob_chars_quoted() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZQM_G", "*(.)", "${(q-)ZQM_G}");
assert_eq!(result, "'*(.)'", "ztst:460 — (q-) quotes glob chars");
}
#[test]
fn paramsubst_zsh_corpus_quoted_zero_length_in_subst() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZZL_F", "", "${${ZZL_F}/?*/replacement}");
assert_eq!(
result, "",
"ztst:1304 — empty var stays empty through nested /"
);
}
#[test]
fn paramsubst_zsh_corpus_colon_minus_empty_uses_default() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZCM_E", "", "${ZCM_E:-fallback}");
assert_eq!(result, "fallback", "${{var:-d}} empty uses default");
}
#[test]
fn paramsubst_zsh_corpus_colon_minus_set_uses_var() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZCM_S", "real", "${ZCM_S:-fallback}");
assert_eq!(result, "real", "${{var:-d}} non-empty returns var");
}
#[test]
fn paramsubst_zsh_corpus_colon_plus_set_uses_alt() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZCP_S", "yes", "${ZCP_S:+alt}");
assert_eq!(result, "alt", "${{var:+a}} non-empty returns alt");
}
#[test]
fn paramsubst_zsh_corpus_colon_plus_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZCP_E", "", "${ZCP_E:+alt}");
assert_eq!(result, "", "${{var:+a}} empty returns empty");
}
#[test]
fn paramsubst_zsh_corpus_dash_only_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZD_E", "", "${ZD_E-fallback}");
assert_eq!(
result, "",
"${{var-d}} set-but-empty returns empty (not default)"
);
}
#[test]
fn paramsubst_zsh_corpus_hash_strip_shortest_prefix() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_S", "hellohello", "${ZP_S#hello}");
assert_eq!(result, "hello", "${{var#pat}} strips shortest prefix");
}
#[test]
fn paramsubst_zsh_corpus_double_hash_strip_longest_prefix() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZP_L", "hellohello", "${ZP_L##h*}");
assert_eq!(result, "", "${{var##pat}} strips longest prefix");
}
#[test]
fn paramsubst_zsh_corpus_percent_strip_shortest_suffix() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZS_S", "hellohello", "${ZS_S%hello}");
assert_eq!(result, "hello", "${{var%pat}} strips shortest suffix");
}
#[test]
fn paramsubst_zsh_corpus_double_percent_strip_longest_suffix() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZS_L", "hellohello", "${ZS_L%%l*}");
assert_eq!(result, "he", "${{var%%pat}} strips longest suffix");
}
#[test]
fn paramsubst_zsh_corpus_u_flag_uppercases_multibyte() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZUM", "ténébreux", "${(U)ZUM}");
assert_eq!(result, "TÉNÉBREUX", "ztst:133 — (U) on accented chars");
}
#[test]
fn paramsubst_zsh_corpus_l_flag_lowercases_multibyte() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZLM", "TÉNÉBREUX", "${(L)ZLM}");
assert_eq!(result, "ténébreux", "ztst:134 — (L) on accented chars");
}
#[test]
fn paramsubst_zsh_corpus_c_flag_capitalizes_multibyte_with_apostrophe() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZCM", "l'état c'est moi", "${(C)ZCM}");
assert_eq!(
result, "L'État C'Est Moi",
"ztst:136 — (C) restarts word after '"
);
}
#[test]
fn paramsubst_zsh_corpus_multibyte_subscript_first() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZMS", "ténébreux", "${ZMS[1]}");
assert_eq!(result, "t", "ztst:21 — [1] is first codepoint, not byte");
}
#[test]
fn paramsubst_zsh_corpus_multibyte_subscript_accented() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZMS2", "ténébreux", "${ZMS2[2]}");
assert_eq!(
result, "é",
"ztst:22 — [2] is 'é' (multibyte codepoint, not byte)"
);
}
#[test]
fn paramsubst_zsh_corpus_multibyte_slice_first_three() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZMSL", "ténébreux", "${ZMSL[1,3]}");
assert_eq!(result, "tén", "ztst:22 — [1,3] = first 3 codepoints");
}
#[test]
fn paramsubst_zsh_corpus_multibyte_pattern_replace_first() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZMR", "ténébreux", "${ZMR/é/X}");
assert_eq!(result, "tXnébreux", "first / replaces first é");
}
#[test]
fn paramsubst_zsh_corpus_multibyte_pattern_replace_all() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZMRG", "ténébreux", "${ZMRG//é/X}");
assert_eq!(result, "tXnXbreux", "// replaces all é");
}
#[test]
fn paramsubst_zsh_corpus_substring_offset_only() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZSS_O", "hello", "${ZSS_O:2}");
assert_eq!(result, "llo", "${{var:2}} skips first 2");
}
#[test]
fn paramsubst_zsh_corpus_substring_offset_length() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZSS_OL", "hello", "${ZSS_OL:1:3}");
assert_eq!(result, "ell", "${{var:1:3}} 3 chars from offset 1");
}
#[test]
fn paramsubst_zsh_corpus_substring_offset_zero() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZSS_Z", "hello", "${ZSS_Z:0}");
assert_eq!(result, "hello", "${{var:0}} = entire string");
}
#[test]
fn paramsubst_zsh_corpus_substring_zero_length() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZSS_E", "hello", "${ZSS_E:0:0}");
assert_eq!(result, "", "${{var:0:0}} = empty");
}
#[test]
fn paramsubst_zsh_corpus_substring_offset_past_end() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZSS_P", "hi", "${ZSS_P:10}");
assert_eq!(result, "", "${{var:past_end}} = empty");
}
#[test]
fn paramsubst_zsh_corpus_substring_negative_offset() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZSS_N", "hello", "${ZSS_N: -2}");
assert_eq!(result, "lo", "${{var: -2}} = last 2 chars");
}
#[test]
fn paramsubst_zsh_corpus_arith_simple_add() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_A", "ignored", "$((1+2))");
assert_eq!(result, "3", "$((1+2)) = '3'");
}
#[test]
fn paramsubst_zsh_corpus_arith_multiply() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_M", "ignored", "$((10*5))");
assert_eq!(result, "50");
}
#[test]
fn paramsubst_zsh_corpus_arith_left_shift() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_LS", "ignored", "$((1 << 4))");
assert_eq!(result, "16");
}
#[test]
fn paramsubst_zsh_corpus_arith_hex_literal() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_H", "ignored", "$((0xff))");
assert_eq!(result, "255");
}
#[test]
fn paramsubst_zsh_corpus_arith_unary_minus() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_N", "ignored", "$((-5))");
assert_eq!(result, "-5");
}
#[test]
fn paramsubst_zsh_corpus_arith_integer_division() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_D", "ignored", "$((100/3))");
assert_eq!(result, "33");
}
#[test]
fn paramsubst_zsh_corpus_arith_modulo() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_MOD", "ignored", "$((10%3))");
assert_eq!(result, "1");
}
#[test]
fn paramsubst_zsh_corpus_arith_power() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZAR_P", "ignored", "$((2**8))");
assert_eq!(result, "256");
}
#[test]
fn paramsubst_zsh_corpus_arith_with_variable() {
let _g = crate::test_util::global_state_lock();
crate::ported::params::unsetparam("ZAR_V");
crate::ported::params::setiparam("ZAR_V", 21);
let result = psubst_one("ZAR_V_IGNORE", "ignored", "$(( ZAR_V * 2 ))");
assert_eq!(result, "42");
crate::ported::params::unsetparam("ZAR_V");
}
#[test]
fn paramsubst_zsh_corpus_type_query_scalar() {
let _g = crate::test_util::global_state_lock();
let result = psubst_one("ZT_S", "hello", "${(t)ZT_S}");
assert!(
result.starts_with("scalar"),
"${{(t)var}} on scalar starts with 'scalar', got: {result:?}",
);
}
#[test]
fn paramsubst_zsh_corpus_type_query_array() {
let _g = crate::test_util::global_state_lock();
let (result, _) = psubst_arr("ZT_A", &["a", "b"], "${(t)ZT_A}");
assert!(
result.starts_with("array"),
"${{(t)var}} on array starts with 'array', got: {result:?}",
);
}
#[test]
fn quotesubst_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotesubst(""), "");
}
#[test]
fn quotesubst_is_pure() {
let _g = crate::test_util::global_state_lock();
for s in ["", "abc", "$var", "no-special"] {
let first = quotesubst(s);
for _ in 0..3 {
assert_eq!(quotesubst(s), first, "quotesubst({:?}) must be pure", s);
}
}
}
#[test]
fn singsub_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(singsub(""), "");
}
#[test]
fn wcpadwidth_returns_i32_type() {
let _: i32 = wcpadwidth('a', 0);
}
#[test]
fn wcpadwidth_ascii_letter_returns_one() {
assert_eq!(wcpadwidth('a', 0), 1, "ASCII letter width = 1");
}
#[test]
fn wcpadwidth_is_pure() {
for c in ['a', '0', ' ', '\t', '日'] {
let first = wcpadwidth(c, 0);
for _ in 0..3 {
assert_eq!(
wcpadwidth(c, 0),
first,
"wcpadwidth({:?}, 0) must be pure",
c
);
}
}
}
#[test]
fn get_strarg_empty_returns_none() {
assert!(get_strarg("").is_none(), "empty → None");
}
#[test]
fn get_intarg_empty_returns_none() {
assert!(get_intarg("").is_none(), "empty → None");
}
#[test]
fn untok_and_escape_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(untok_and_escape("", false, false), "");
}
#[test]
fn sub_flags_get_set_round_trip() {
let _g = crate::test_util::global_state_lock();
let saved = sub_flags_get();
sub_flags_set(0x42);
assert_eq!(sub_flags_get(), 0x42, "sub_flags round-trips");
sub_flags_set(saved);
}
#[test]
fn check_colon_subscript_empty_returns_none_pin() {
let _g = crate::test_util::global_state_lock();
assert!(check_colon_subscript("").is_none(), "empty → None");
}
}
pub static NULSTRING_BYTES: [char; 2] = [Nularg, '\0'];
fn exec_sethparam(name: &str, parts: Vec<String>) {
let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
let mut it = parts.into_iter();
while let (Some(k), Some(v)) = (it.next(), it.next()) {
map.insert(k, v);
}
if let Ok(mut store) = paramtab_hashed_storage().lock() {
store.insert(name.to_string(), map);
}
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(name) {
pm.node.flags |= PM_HASHED as i32;
} else {
let pm: Param = Box::new(param {
node: hashnode {
next: None,
nam: name.to_string(),
flags: PM_HASHED as i32,
},
u_data: 0,
u_arr: None,
u_str: None,
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
});
tab.insert(name.to_string(), pm);
}
}
}
fn exec_sync_state_from_paramtab() {}
fn exec_getsparam(name: &str) -> Option<String> {
crate::ported::params::getsparam(name)
}
pub fn sub_flags_get() -> i32 {
SUB_FLAGS.with(|c| c.get())
}
pub fn sub_flags_set(v: i32) {
SUB_FLAGS.with(|c| c.set(v));
}