use std::collections::VecDeque;
pub mod tokens {
pub const POUND: char = '\u{80}'; pub const STRING: char = '\u{81}'; pub const QSTRING: char = '\u{82}'; pub const TICK: char = '\u{83}'; pub const QTICK: char = '\u{84}'; pub const INPAR: char = '\u{85}'; pub const OUTPAR: char = '\u{86}'; pub const INBRACE: char = '\u{87}'; pub const OUTBRACE: char = '\u{88}'; pub const INBRACK: char = '\u{89}'; pub const OUTBRACK: char = '\u{8A}'; pub const INANG: char = '\u{8B}'; pub const OUTANG: char = '\u{8C}'; pub const OUTANGPROC: char = '\u{8D}'; pub const EQUALS: char = '\u{8E}'; pub const NULARG: char = '\u{8F}'; pub const INPARMATH: char = '\u{90}'; pub const OUTPARMATH: char = '\u{91}'; pub const SNULL: char = '\u{92}'; pub const MARKER: char = '\u{93}'; pub const BNULL: char = '\u{94}';
pub fn is_token(c: char) -> bool {
c as u32 >= 0x80 && c as u32 <= 0x94
}
pub fn token_to_char(c: char) -> char {
match c {
POUND => '#',
STRING | QSTRING => '$',
TICK | QTICK => '`',
INPAR => '(',
OUTPAR => ')',
INBRACE => '{',
OUTBRACE => '}',
INBRACK => '[',
OUTBRACK => ']',
INANG => '<',
OUTANG => '>',
EQUALS => '=',
_ => c,
}
}
}
use tokens::*;
pub const LF_ARRAY: u32 = 1;
pub mod prefork_flags {
pub const SINGLE: u32 = 1; pub const SPLIT: u32 = 2; pub const SHWORDSPLIT: u32 = 4; pub const NOSHWORDSPLIT: u32 = 8; pub const ASSIGN: u32 = 16; pub const TYPESET: u32 = 32; pub const SUBEXP: u32 = 64; pub const KEY_VALUE: u32 = 128; pub const NO_UNTOK: u32 = 256; }
#[derive(Debug, Clone)]
pub struct LinkNode {
pub data: String,
}
#[derive(Debug, Clone, Default)]
pub struct LinkList {
pub nodes: VecDeque<LinkNode>,
pub flags: u32,
}
impl LinkList {
pub fn new() -> Self {
LinkList {
nodes: VecDeque::new(),
flags: 0,
}
}
pub fn from_string(s: &str) -> Self {
let mut list = LinkList::new();
list.nodes.push_back(LinkNode {
data: s.to_string(),
});
list
}
pub fn first_node(&self) -> Option<usize> {
if self.nodes.is_empty() {
None
} else {
Some(0)
}
}
pub fn get_data(&self, idx: usize) -> Option<&str> {
self.nodes.get(idx).map(|n| n.data.as_str())
}
pub fn set_data(&mut self, idx: usize, data: String) {
if let Some(node) = self.nodes.get_mut(idx) {
node.data = data;
}
}
pub fn insert_after(&mut self, idx: usize, data: String) -> usize {
self.nodes.insert(idx + 1, LinkNode { data });
idx + 1
}
pub fn remove(&mut self, idx: usize) {
if idx < self.nodes.len() {
self.nodes.remove(idx);
}
}
pub fn next_node(&self, idx: usize) -> Option<usize> {
if idx + 1 < self.nodes.len() {
Some(idx + 1)
} else {
None
}
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn len(&self) -> usize {
self.nodes.len()
}
}
#[derive(Default)]
pub struct SubstState {
pub errflag: bool,
pub opts: SubstOptions,
pub variables: std::collections::HashMap<String, String>,
pub arrays: std::collections::HashMap<String, Vec<String>>,
pub assoc_arrays: std::collections::HashMap<String, indexmap::IndexMap<String, String>>,
pub skip_filesub: bool,
pub function_names: std::collections::HashSet<String>,
pub command_names: std::collections::HashSet<String>,
pub alias_names: std::collections::HashSet<String>,
}
impl SubstState {
pub fn from_executor(exec: &crate::exec::ShellExecutor) -> Self {
let assoc_arrays: std::collections::HashMap<
String,
indexmap::IndexMap<String, String>,
> = exec
.assoc_arrays
.iter()
.map(|(k, v)| {
(
k.clone(),
v.iter().map(|(ik, iv)| (ik.clone(), iv.clone())).collect(),
)
})
.collect();
let function_names: std::collections::HashSet<String> = exec
.function_names()
.into_iter()
.collect();
let alias_names: std::collections::HashSet<String> =
exec.aliases.keys().cloned().collect();
let command_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut arrays = exec.arrays.clone();
if !exec.positional_params.is_empty() {
arrays
.entry("@".to_string())
.or_insert_with(|| exec.positional_params.clone());
arrays
.entry("*".to_string())
.or_insert_with(|| exec.positional_params.clone());
arrays
.entry("argv".to_string())
.or_insert_with(|| exec.positional_params.clone());
}
SubstState {
errflag: false,
opts: SubstOptions::default(),
variables: exec.variables.clone(),
arrays,
assoc_arrays,
skip_filesub: false,
function_names,
command_names,
alias_names,
}
}
pub fn commit_to_executor(self, exec: &mut crate::exec::ShellExecutor) {
if self.errflag {
return;
}
exec.variables = self.variables;
exec.arrays = self.arrays;
for (name, new_map) in self.assoc_arrays {
let entry = exec
.assoc_arrays
.entry(name.clone())
.or_default();
for k in entry.keys().cloned().collect::<Vec<_>>() {
if let Some(v) = new_map.get(&k) {
entry.insert(k, v.clone());
}
}
for (k, v) in &new_map {
if !entry.contains_key(k) {
entry.insert(k.clone(), v.clone());
}
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SubstOptions {
pub sh_file_expansion: bool,
pub sh_word_split: bool,
pub ignore_braces: bool,
pub glob_subst: bool,
pub ksh_typeset: bool,
pub exec_opt: bool,
}
pub const NULSTRING: &str = "\u{8F}";
fn keyvalpairelement(list: &mut LinkList, node_idx: usize) -> Option<usize> {
let data = list.get_data(node_idx)?;
let chars: Vec<char> = data.chars().collect();
if chars.is_empty() || chars[0] != INBRACK {
return None;
}
let mut end_pos = None;
for (i, &c) in chars.iter().enumerate().skip(1) {
if c == OUTBRACK {
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);
let is_assign = chars.get(end_pos + 1) == Some(&EQUALS);
if !is_assign && !is_append {
return None;
}
let key: String = chars[1..end_pos].iter().collect();
let value_start = if is_append { end_pos + 3 } else { end_pos + 2 };
let value: String = chars[value_start..].iter().collect();
let marker = if is_append {
format!("{}+", MARKER)
} else {
MARKER.to_string()
};
list.set_data(node_idx, marker);
let key_idx = list.insert_after(node_idx, key);
let val_idx = list.insert_after(key_idx, value);
Some(val_idx)
}
pub fn prefork(list: &mut LinkList, flags: u32, ret_flags: &mut u32, state: &mut SubstState) {
let mut node_idx = 0;
let mut stop_idx: Option<usize> = None;
let mut keep = false;
let asssub = (flags & prefork_flags::TYPESET != 0) && state.opts.ksh_typeset;
let mut iter_count = 0u32;
while node_idx < list.len() {
iter_count += 1;
if iter_count > 100_000 {
return;
}
if (flags & (prefork_flags::SINGLE | prefork_flags::ASSIGN)) == prefork_flags::ASSIGN {
if let Some(new_idx) = keyvalpairelement(list, node_idx) {
node_idx = new_idx + 1;
*ret_flags |= prefork_flags::KEY_VALUE;
continue;
}
}
if state.errflag {
return;
}
if state.opts.sh_file_expansion {
if let Some(data) = list.get_data(node_idx) {
let new_data = filesub(
data,
flags & (prefork_flags::TYPESET | prefork_flags::ASSIGN),
state,
);
list.set_data(node_idx, new_data);
}
} else {
if let Some(new_idx) = stringsubst(
list,
node_idx,
flags & !(prefork_flags::TYPESET | prefork_flags::ASSIGN),
ret_flags,
asssub,
state,
) {
node_idx = new_idx;
} else {
return;
}
}
node_idx += 1;
}
if state.opts.sh_file_expansion {
node_idx = 0;
while node_idx < list.len() {
if let Some(new_idx) = stringsubst(
list,
node_idx,
flags & !(prefork_flags::TYPESET | prefork_flags::ASSIGN),
ret_flags,
asssub,
state,
) {
node_idx = new_idx + 1;
} else {
return;
}
}
}
node_idx = 0;
while node_idx < list.len() {
if Some(node_idx) == stop_idx {
keep = false;
}
if let Some(data) = list.get_data(node_idx) {
if !data.is_empty() {
let data = remnulargs(data);
list.set_data(node_idx, data.clone());
if !state.opts.ignore_braces && (flags & prefork_flags::SINGLE == 0) {
if !keep {
stop_idx = list.next_node(node_idx);
}
while hasbraces(list.get_data(node_idx).unwrap_or("")) {
keep = true;
xpandbraces(list, &mut node_idx);
}
}
if !state.opts.sh_file_expansion && !state.skip_filesub {
if let Some(data) = list.get_data(node_idx) {
let new_data = filesub(
data,
flags & (prefork_flags::TYPESET | prefork_flags::ASSIGN),
state,
);
list.set_data(node_idx, new_data);
}
}
} else if (flags & prefork_flags::SINGLE == 0)
&& (*ret_flags & prefork_flags::KEY_VALUE == 0)
&& !keep
{
list.remove(node_idx);
continue; }
}
if state.errflag {
return;
}
node_idx += 1;
}
}
fn stringsubstquote(strstart: &str, strdpos: usize) -> (String, usize) {
let chars: Vec<char> = strstart.chars().collect();
let start = strdpos + 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 processed = getkeystring(&content);
let prefix: String = chars[..strdpos].iter().collect();
let suffix: String = if end + 1 < chars.len() {
chars[end + 1..].iter().collect()
} else {
String::new()
};
let result = format!("{}{}{}", prefix, processed, suffix);
let new_pos = strdpos + processed.len();
(result, new_pos)
}
pub fn getkeystring_pub(s: &str) -> String {
getkeystring(s)
}
pub fn magic_assoc_keys_from_executor(
name: &str,
exec: &crate::exec::ShellExecutor,
) -> Option<Vec<String>> {
let state = SubstState::from_executor(exec);
magic_assoc_keys(name, &state)
}
pub fn magic_assoc_keys(name: &str, state: &SubstState) -> Option<Vec<String>> {
use std::collections::HashSet;
fn sorted_set(set: &HashSet<String>) -> Vec<String> {
let mut v: Vec<String> = set.iter().cloned().collect();
v.sort();
v
}
Some(match name {
"aliases" => sorted_set(&state.alias_names),
"functions" => sorted_set(&state.function_names),
"commands" => sorted_set(&state.command_names),
"options" => {
let mut v: Vec<String> =
crate::exec::with_executor(|exec| exec.options.keys().cloned().collect());
v.sort();
v
}
"parameters" => {
let mut set: HashSet<String> = state.variables.keys().cloned().collect();
for k in state.arrays.keys() {
set.insert(k.clone());
}
for k in state.assoc_arrays.keys() {
set.insert(k.clone());
}
sorted_set(&set)
}
"terminfo" => crate::modules::terminfo::COMMON_STRING_CAPS
.iter()
.map(|s| (*s).to_string())
.collect(),
"termcap" => {
let mut v: Vec<String> = Vec::new();
v.extend(
crate::modules::termcap::BOOL_CODES
.iter()
.map(|s| s.to_string()),
);
v.extend(
crate::modules::termcap::NUM_CODES
.iter()
.map(|s| s.to_string()),
);
v.extend(
crate::modules::termcap::STR_CODES
.iter()
.map(|s| s.to_string()),
);
v
}
"errnos" => crate::modules::system::ERRNO_NAMES
.iter()
.map(|(n, _)| (*n).to_string())
.collect(),
"sysparams" => vec![
"pid".to_string(),
"ppid".to_string(),
"procsubstpid".to_string(),
],
_ => return None,
})
}
fn check_magic_assoc_set(name: &str, key: &str, state: &SubstState) -> bool {
match name {
"functions" | "dis_functions" => state.function_names.contains(key),
"aliases" | "dis_aliases" | "galiases" | "saliases" => state.alias_names.contains(key),
"commands" => state.command_names.contains(key),
_ => false,
}
}
fn getkeystring(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('r') => result.push('\r'),
Some('\\') => result.push('\\'),
Some('\'') => result.push('\''),
Some('"') => result.push('"'),
Some('a') => result.push('\x07'),
Some('b') => result.push('\x08'),
Some('e') | Some('E') => result.push('\x1b'),
Some('f') => result.push('\x0c'),
Some('v') => result.push('\x0b'),
Some('0') => {
let mut val = 0u32;
for _ in 0..3 {
if let Some(&c) = chars.peek() {
if ('0'..='7').contains(&c) {
val = val * 8 + (c as u32 - '0' as u32);
chars.next();
} else {
break;
}
}
}
if let Some(ch) = char::from_u32(val) {
result.push(ch);
}
}
Some('x') => {
let mut val = 0u32;
for _ in 0..2 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
val = val * 16 + c.to_digit(16).unwrap();
chars.next();
} else {
break;
}
}
}
if let Some(ch) = char::from_u32(val) {
result.push(ch);
}
}
Some('u') => {
let mut val = 0u32;
for _ in 0..4 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
val = val * 16 + c.to_digit(16).unwrap();
chars.next();
} else {
break;
}
}
}
if let Some(ch) = char::from_u32(val) {
result.push(ch);
}
}
Some('U') => {
let mut val = 0u32;
for _ in 0..8 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
val = val * 16 + c.to_digit(16).unwrap();
chars.next();
} else {
break;
}
}
}
if let Some(ch) = char::from_u32(val) {
result.push(ch);
}
}
Some(c) => result.push(c),
None => result.push('\\'),
}
} else {
result.push(c);
}
}
result
}
fn stringsubst(
list: &mut LinkList,
node_idx: usize,
pf_flags: u32,
ret_flags: &mut u32,
asssub: bool,
state: &mut SubstState,
) -> Option<usize> {
let mut str3 = list.get_data(node_idx)?.to_string();
let mut pos = 0;
let mut p1_iter = 0u32;
loop {
if state.errflag {
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)
{
let (subst, rest) = if c == INANG || c == OUTANGPROC {
getproc(&str3[pos..], state)
} else {
getoutputfile(&str3[pos..], state)
};
if state.errflag {
return None;
}
let subst = subst.unwrap_or_default();
let prefix: String = chars[..pos].iter().collect();
str3 = format!("{}{}{}", prefix, subst, rest);
pos += subst.len();
list.set_data(node_idx, str3.clone());
continue;
}
pos += 1;
}
pos = 0;
let mut iter_count = 0u32;
loop {
if state.errflag {
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.set_data(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.set_data(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.set_data(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.set_data(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_is(INPAR, '(') || next_is(INPARMATH, '\0') {
if !qt {
list.flags |= LF_ARRAY;
}
pos += 1;
let (result, new_pos) = process_command_subst(&str3, pos, qt, state);
str3 = result;
pos = new_pos;
list.set_data(node_idx, str3.clone());
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 { ']' };
if let Some(end) = find_matching_bracket(&str3[start..], open, close) {
let expr: String = str3.chars().skip(start).take(end).collect();
let value = arithsubst(&expr, state);
let prefix: String = str3.chars().take(pos).collect();
let suffix: String = str3.chars().skip(start + end + 1).collect();
str3 = format!("{}{}{}", prefix, value, suffix);
list.set_data(node_idx, str3.clone());
continue;
} else {
state.errflag = true;
eprintln!("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.set_data(node_idx, str3.clone());
continue;
} else {
let mut new_pf_flags = pf_flags;
if (state.opts.sh_word_split && (pf_flags & prefork_flags::NOSHWORDSPLIT == 0))
|| (pf_flags & prefork_flags::SPLIT != 0)
{
new_pf_flags |= prefork_flags::SHWORDSPLIT;
}
let (new_str, new_pos, new_nodes) = paramsubst(
&str3,
pos,
qt,
new_pf_flags
& (prefork_flags::SINGLE
| prefork_flags::SHWORDSPLIT
| prefork_flags::SUBEXP),
ret_flags,
state,
);
if state.errflag {
return None;
}
let mut current_idx = node_idx;
for (i, node_data) in new_nodes.into_iter().enumerate() {
if i == 0 {
list.set_data(current_idx, node_data);
} else {
current_idx = list.insert_after(current_idx, node_data);
}
}
str3 = list.get_data(node_idx)?.to_string();
pos = new_pos;
continue;
}
}
let qt = c == QTICK;
if qt || c == TICK {
if !qt {
list.flags |= LF_ARRAY;
}
let (result, new_pos) = process_backtick_subst(&str3, pos, qt, pf_flags, state);
str3 = result;
pos = new_pos;
list.set_data(node_idx, str3.clone());
continue;
}
if asssub && (c == '=' || c == EQUALS) && pos > 0 {
}
pos += 1;
}
if state.errflag {
None
} else {
Some(node_idx)
}
}
pub fn substitute_brace(content: &str, exec: &mut crate::exec::ShellExecutor) -> String {
exec.in_paramsubst_nest += 1;
let mut state = SubstState::from_executor(exec);
let wrapped = format!("${{{}}}", content);
let (result, _pos, _nodes) =
paramsubst(&wrapped, 0, false, 0, &mut 0, &mut state);
state.commit_to_executor(exec);
exec.in_paramsubst_nest -= 1;
result
}
fn process_command_subst(
s: &str,
start_pos: usize,
qt: bool,
state: &mut SubstState,
) -> (String, usize) {
let chars: Vec<char> = s.chars().collect();
let c = chars.get(start_pos).copied().unwrap_or('\0');
if c == INPARMATH {
let expr_start = start_pos + 1;
if let Some(end) = find_matching_parmath(&s[expr_start..]) {
let expr: String = s.chars().skip(expr_start).take(end).collect();
let value = arithsubst(&expr, state);
let prefix: String = s.chars().take(start_pos - 1).collect();
let suffix: String = s.chars().skip(expr_start + end + 1).collect();
return (
format!("{}{}{}", prefix, value, suffix),
prefix.len() + value.len(),
);
}
}
if let Some(end) = find_matching_bracket(&s[start_pos..], INPAR, OUTPAR) {
let cmd: String = s.chars().skip(start_pos + 1).take(end - 1).collect();
let output = if state.opts.exec_opt {
run_command(&cmd)
} else {
String::new()
};
let output = output.trim_end_matches('\n');
let prefix: String = s.chars().take(start_pos - 1).collect();
let suffix: String = s.chars().skip(start_pos + end + 1).collect();
return (
format!("{}{}{}", prefix, output, suffix),
prefix.len() + output.len(),
);
}
(s.to_string(), start_pos + 1)
}
fn process_backtick_subst(
s: &str,
start_pos: usize,
_qt: bool,
_pf_flags: u32,
state: &mut SubstState,
) -> (String, usize) {
let chars: Vec<char> = s.chars().collect();
let end_char = chars[start_pos];
let mut end_pos = start_pos + 1;
while end_pos < chars.len() && chars[end_pos] != end_char {
end_pos += 1;
}
if end_pos >= chars.len() {
state.errflag = true;
eprintln!("failed to find end of command substitution");
return (s.to_string(), start_pos + 1);
}
let cmd: String = chars[start_pos + 1..end_pos].iter().collect();
let output = run_command(&cmd);
let output = output.trim_end_matches('\n');
let prefix: String = chars[..start_pos].iter().collect();
let suffix: String = chars[end_pos + 1..].iter().collect();
(
format!("{}{}{}", prefix, output, suffix),
prefix.len() + output.len(),
)
}
fn paramsubst(
s: &str,
start_pos: usize,
qt: bool,
pf_flags: u32,
ret_flags: &mut u32,
state: &mut SubstState,
) -> (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;
return parse_brace_param(s, start_pos, pos, qt, pf_flags, ret_flags, state);
}
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;
if chars.get(pos).copied() == Some('[') {
let mut depth = 1;
let mut q = pos + 1;
while q < chars.len() && depth > 0 {
match chars[q] {
'[' => depth += 1,
']' => {
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_no_tilde(&raw_sub, state));
pos = q + 1;
}
}
let value = if let Some(sub) = subscript_str.as_deref() {
let v = get_param_with_subscript(&var_name, Some(sub), state);
v.join(" ")
} else {
get_param_value(&var_name, state)
};
if pf_flags & prefork_flags::SHWORDSPLIT != 0 && !qt {
let words = split_words(&value, state);
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 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 = state
.variables
.get("?")
.cloned()
.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)
}
'#' => {
let value = state
.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 values = state.arrays.get("@").cloned().unwrap_or_default();
let value = if c == '*' || qt {
values.join(" ")
} else {
if pf_flags & prefork_flags::SINGLE == 0 {
let prefix: String = chars[..start_pos].iter().collect();
let suffix: String = chars[pos + 1..].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[pos + 1..].iter().collect();
let result = format!("{}{}{}", prefix, value, suffix);
result_nodes.push(result.clone());
(result, prefix.len() + value.len(), result_nodes)
}
'0'..='9' => {
let digit = c.to_digit(10).unwrap() as usize;
let value = state
.arrays
.get("@")
.and_then(|a| a.get(digit))
.cloned()
.unwrap_or_default();
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)
}
_ => {
result_nodes.push(s.to_string());
(s.to_string(), start_pos + 1, result_nodes)
}
}
}
fn parse_brace_param(
s: &str,
dollar_pos: usize,
brace_pos: usize,
qt: bool,
pf_flags: u32,
_ret_flags: &mut u32,
state: &mut SubstState,
) -> (String, usize, Vec<String>) {
let chars: Vec<char> = s.chars().collect();
let mut pos = brace_pos;
let mut result_nodes = Vec::new();
let mut flags = ParamFlags::default();
if chars.get(pos) == Some(&'(') {
pos += 1;
while pos < chars.len() && chars[pos] != ')' {
let flag_char = chars[pos];
match flag_char {
'L' => flags.lowercase = true,
'U' => flags.uppercase = true,
'C' => flags.capitalize = true,
'u' => flags.unique = true,
'o' => flags.sort = true,
'O' => flags.sort_reverse = true,
'a' => flags.sort_array_index = true,
'i' => flags.sort_case_insensitive = true,
'n' => flags.sort_numeric = true,
'k' => flags.keys = true,
'v' => flags.values = true,
't' => flags.type_info = true,
'P' => flags.prompt_expand = true,
'e' => flags.eval = true,
'q' => flags.quote_level += 1,
'Q' => flags.unquote = true,
'X' => flags.report_error = true,
'z' => flags.split_words = true,
'f' => flags.split_lines = true,
'F' => flags.join_lines = true,
'w' => flags.count_words = true,
'W' => flags.count_words_null = true,
'c' => flags.count_chars = true,
'#' => flags.length_chars = true,
'%' => flags.prompt_percent = true,
'A' => flags.create_assoc = true,
'@' => flags.array_expand = true,
'~' => flags.glob_subst = true,
'V' => flags.visible = true,
'S' | 'I' => flags.search = true,
'M' => flags.match_flag = true,
'R' => flags.reverse_subscript = true,
'B' | 'E' | 'N' => flags.begin_end_length = true,
's' => {
pos += 1;
if pos < chars.len()
&& !chars[pos].is_ascii_alphanumeric()
&& chars[pos] != ')'
{
let del = chars[pos];
pos += 1;
let mut sep = String::new();
while pos < chars.len() && chars[pos] != del {
sep.push(chars[pos]);
pos += 1;
}
flags.split_sep = Some(sep);
if pos < chars.len() && chars[pos] == del {
pos += 1;
}
pos = pos.saturating_sub(1);
} else {
pos -= 1;
}
}
'j' => {
pos += 1;
if pos < chars.len()
&& !chars[pos].is_ascii_alphanumeric()
&& chars[pos] != ')'
{
let del = chars[pos];
pos += 1;
let mut sep = String::new();
while pos < chars.len() && chars[pos] != del {
sep.push(chars[pos]);
pos += 1;
}
flags.join_sep = Some(sep);
if pos < chars.len() && chars[pos] == del {
pos += 1;
}
pos = pos.saturating_sub(1);
} else {
pos -= 1;
}
}
'l' => {
pos += 1;
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut len_str = String::new();
while pos < chars.len() && chars[pos].is_ascii_digit() {
len_str.push(chars[pos]);
pos += 1;
}
if let Ok(len) = len_str.parse() {
flags.pad_left = Some(len);
}
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut s1 = String::new();
while pos < chars.len()
&& chars[pos] != ':'
&& chars[pos] != ')'
{
s1.push(chars[pos]);
pos += 1;
}
flags.pad_string1 = Some(s1);
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut s2 = String::new();
while pos < chars.len()
&& chars[pos] != ':'
&& chars[pos] != ')'
{
s2.push(chars[pos]);
pos += 1;
}
flags.pad_string2 = Some(s2);
}
}
pos = pos.saturating_sub(1);
} else {
pos -= 1;
}
}
'r' => {
pos += 1;
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut len_str = String::new();
while pos < chars.len() && chars[pos].is_ascii_digit() {
len_str.push(chars[pos]);
pos += 1;
}
if let Ok(len) = len_str.parse() {
flags.pad_right = Some(len);
}
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut s1 = String::new();
while pos < chars.len()
&& chars[pos] != ':'
&& chars[pos] != ')'
{
s1.push(chars[pos]);
pos += 1;
}
flags.pad_string1 = Some(s1);
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut s2 = String::new();
while pos < chars.len()
&& chars[pos] != ':'
&& chars[pos] != ')'
{
s2.push(chars[pos]);
pos += 1;
}
flags.pad_string2 = Some(s2);
}
}
pos = pos.saturating_sub(1);
} else {
pos -= 1;
}
}
_ => {}
}
pos += 1;
}
if pos < chars.len() {
pos += 1; }
}
let length_prefix = chars.get(pos) == Some(&'#');
if length_prefix {
pos += 1;
}
let mut chkset = false;
if chars.get(pos) == Some(&'+') {
let next = chars.get(pos + 1).copied().unwrap_or('\0');
if next.is_ascii_alphabetic() || next == '_' || next == '{' || next == INBRACE {
chkset = true;
pos += 1;
}
}
let mut nested_value: Option<Vec<String>> = None;
let var_start = pos;
if pos < chars.len()
&& chars[pos] == '$'
&& pos + 1 < chars.len()
&& (chars[pos + 1] == '{' || chars[pos + 1] == INBRACE)
{
let nested_start = pos;
let mut depth = 0;
let mut p = pos;
while p < chars.len() {
let c = chars[p];
if c == '{' || c == INBRACE {
depth += 1;
} else if c == '}' || c == OUTBRACE {
depth -= 1;
if depth == 0 {
p += 1;
break;
}
}
p += 1;
}
let nested_str: String = chars[nested_start..p].iter().collect();
let mut inner_rf = 0u32;
let (resolved, _, nodes) = paramsubst(
&nested_str,
0,
qt,
pf_flags,
&mut inner_rf,
state,
);
nested_value = if nodes.is_empty() {
Some(vec![resolved])
} else {
Some(nodes)
};
pos = p;
} else if pos < chars.len()
&& chars[pos] == '$'
&& pos + 1 < chars.len()
&& chars[pos + 1] == '('
{
let cmd_start = pos;
let mut depth = 0;
let mut p = pos + 2; depth = 1;
while p < chars.len() && depth > 0 {
match chars[p] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
p += 1;
break;
}
}
_ => {}
}
p += 1;
}
let body: String = chars[cmd_start + 2..p.saturating_sub(1)]
.iter()
.collect();
let captured = crate::exec::with_executor(|exec| {
exec.run_command_substitution(&body)
});
let captured = captured.trim_end_matches('\n').to_string();
nested_value = Some(vec![captured]);
pos = p;
} else {
if pos < chars.len()
&& matches!(chars[pos], '@' | '*' | '#' | '?' | '$' | '!')
{
pos += 1;
} else {
while pos < chars.len() {
let c = chars[pos];
if c.is_ascii_alphanumeric() || c == '_' {
pos += 1;
} else {
break;
}
}
}
}
let var_name: String = chars[var_start..pos].iter().collect();
if var_name == "!"
&& pos < chars.len()
&& chars[pos] != '}'
&& chars[pos] != OUTBRACE
{
eprintln!("zshrs:1: bad substitution");
state.errflag = true;
let prefix: String = chars[..dollar_pos].iter().collect();
return (prefix, dollar_pos, Vec::new());
}
let mut subscript = None;
let mut extra_subs: Vec<String> = Vec::new();
while chars.get(pos) == Some(&'[') || chars.get(pos) == Some(&INBRACK) {
pos += 1;
let sub_start = pos;
let mut depth = 1;
while pos < chars.len() && depth > 0 {
let c = chars[pos];
if c == '[' || c == INBRACK {
depth += 1;
} else if c == ']' || c == OUTBRACK {
depth -= 1;
}
if depth > 0 {
pos += 1;
}
}
let captured: String = chars[sub_start..pos].iter().collect();
if subscript.is_none() {
subscript = Some(captured);
} else {
extra_subs.push(captured);
}
pos += 1; }
let mut operator = None;
let mut operand = String::new();
if pos < chars.len() {
let c = chars[pos];
match c {
':' => {
pos += 1;
if pos < chars.len() {
match chars[pos] {
'-' => {
operator = Some(":-");
pos += 1;
}
'=' => {
operator = Some(":=");
pos += 1;
}
'+' => {
operator = Some(":+");
pos += 1;
}
'?' => {
operator = Some(":?");
pos += 1;
}
'#' => {
operator = Some(":#");
pos += 1;
}
'^' if pos + 1 < chars.len() && chars[pos + 1] == '^' => {
operator = Some(":^^");
pos += 2;
}
'^' => {
operator = Some(":^");
pos += 1;
}
':' if pos + 1 < chars.len() && chars[pos + 1] == '=' => {
operator = Some("::=");
pos += 2;
}
c if c.is_alphabetic() || c == '&' => {
operator = Some(":mod");
}
_ => {
operator = Some(":");
} }
}
}
'-' => {
operator = Some("-");
pos += 1;
}
'=' => {
operator = Some("=");
pos += 1;
}
'+' => {
operator = Some("+");
pos += 1;
}
'?' => {
operator = Some("?");
pos += 1;
}
'#' => {
pos += 1;
if chars.get(pos) == Some(&'#') {
operator = Some("##");
pos += 1;
} else {
operator = Some("#");
}
}
'%' => {
pos += 1;
if chars.get(pos) == Some(&'%') {
operator = Some("%%");
pos += 1;
} else {
operator = Some("%");
}
}
'/' => {
pos += 1;
if chars.get(pos) == Some(&'/') {
operator = Some("//");
pos += 1;
} else if chars.get(pos) == Some(&'#') {
operator = Some("/#");
pos += 1;
} else if chars.get(pos) == Some(&'%') {
operator = Some("/%");
pos += 1;
} else {
operator = Some("/");
}
}
'^' => {
pos += 1;
if chars.get(pos) == Some(&'^') {
operator = Some("^^");
pos += 1;
} else {
operator = Some("^");
}
}
',' => {
pos += 1;
if chars.get(pos) == Some(&',') {
operator = Some(",,");
pos += 1;
} else {
operator = Some(",");
}
}
'@' => {
operator = Some("@op");
pos += 1;
}
_ => {}
}
}
let mut depth = 1;
while pos < chars.len() && depth > 0 {
let c = chars[pos];
if c == '{' || c == INBRACE {
depth += 1;
operand.push(c);
} else if c == '}' || c == OUTBRACE {
depth -= 1;
if depth > 0 {
operand.push(c);
}
} else {
operand.push(c);
}
pos += 1;
}
let mut value = if let Some(v) = nested_value.take() {
if flags.prompt_expand {
let target_name = v.first().cloned().unwrap_or_default();
if target_name.is_empty() {
Vec::new()
} else {
get_param_with_subscript(&target_name, subscript.as_deref(), state)
}
} else
if let Some(sub) = subscript.as_deref() {
let resolved_sub = singsub_no_tilde(sub, state);
if resolved_sub == "@" || resolved_sub == "*" {
v
} else if let Ok(idx) = resolved_sub.parse::<i64>() {
let arr = &v;
let n = arr.len() as i64;
let real = if idx > 0 {
(idx - 1) as usize
} else if idx < 0 {
let off = n + idx;
if off < 0 {
return (
chars[..dollar_pos].iter().collect::<String>()
+ &chars[pos..].iter().collect::<String>(),
dollar_pos,
Vec::new(),
);
}
off as usize
} else {
0
};
arr.get(real).cloned().into_iter().collect()
} else {
Vec::new()
}
} else {
v
}
} else if flags.keys && flags.values && state.assoc_arrays.contains_key(&var_name) {
state
.assoc_arrays
.get(&var_name)
.map(|m| {
let mut out = Vec::with_capacity(m.len() * 2);
for (k, v) in m.iter() {
out.push(k.clone());
out.push(v.clone());
}
out
})
.unwrap_or_default()
} else if flags.keys && state.assoc_arrays.contains_key(&var_name) {
state
.assoc_arrays
.get(&var_name)
.map(|m| m.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default()
} else if flags.keys {
magic_assoc_keys(&var_name, state).unwrap_or_default()
} else if flags.values && state.assoc_arrays.contains_key(&var_name) {
state
.assoc_arrays
.get(&var_name)
.map(|m| m.values().cloned().collect::<Vec<_>>())
.unwrap_or_default()
} else if flags.prompt_expand {
let target_name = if let Some(v) = nested_value.take() {
v.first().cloned().unwrap_or_default()
} else {
get_param_value(&var_name, state)
};
if subscript.is_some() {
get_param_with_subscript(&target_name, subscript.as_deref(), state)
} else if target_name.is_empty() {
Vec::new()
} else {
get_param_with_subscript(&target_name, None, state)
}
} else if flags.type_info {
let ty = if state.assoc_arrays.contains_key(&var_name) {
"association"
} else if state.arrays.contains_key(&var_name) {
"array"
} else if state.variables.contains_key(&var_name) {
"scalar"
} else {
""
};
if ty.is_empty() {
Vec::new()
} else {
vec![ty.to_string()]
}
} else if subscript.is_some() || !var_name.is_empty() {
get_param_with_subscript(&var_name, subscript.as_deref(), state)
} else {
Vec::new()
};
for extra in &extra_subs {
value = apply_chained_subscript(value, extra, state);
}
if chkset {
let is_set = if let Some(sub) = subscript.as_deref() {
if let Some(arr) = state.arrays.get(&var_name) {
if sub == "@" || sub == "*" {
!arr.is_empty()
} else if let Ok(idx) = sub.parse::<i64>() {
let real = if idx > 0 {
(idx - 1) as usize
} else if idx < 0 {
let off = arr.len() as i64 + idx;
if off < 0 {
0
} else {
off as usize
}
} else {
return (
chars[..dollar_pos].iter().collect::<String>()
+ "0"
+ &chars[pos..].iter().collect::<String>(),
dollar_pos + 1,
vec!["0".to_string()],
);
};
real < arr.len()
} else {
false
}
} else if let Some(map) = state.assoc_arrays.get(&var_name) {
if sub == "@" || sub == "*" {
!map.is_empty()
} else {
map.contains_key(sub)
}
} else {
let resolved = singsub_no_tilde(sub, state);
check_magic_assoc_set(&var_name, &resolved, state)
}
} else {
state.variables.contains_key(&var_name)
|| state.arrays.contains_key(&var_name)
|| state.assoc_arrays.contains_key(&var_name)
|| std::env::var(&var_name).is_ok()
};
let s = if is_set { "1" } else { "0" };
let prefix: String = chars[..dollar_pos].iter().collect();
let suffix: String = chars[pos..].iter().collect();
return (
format!("{}{}{}", prefix, s, suffix),
dollar_pos + 1,
vec![s.to_string()],
);
}
value = apply_operator_with_flags(
&var_name,
subscript.as_deref(),
value,
operator,
&operand,
flags.match_flag,
state,
);
if length_prefix {
let len = if flags.count_chars {
value.iter().map(|s| s.chars().count()).sum::<usize>()
} else if flags.count_words {
value
.iter()
.map(|s| s.split_whitespace().count())
.sum::<usize>()
} else if value.len() == 1 {
value[0].chars().count()
} else {
value.len()
};
value = vec![len.to_string()];
flags.count_chars = false;
flags.count_words = false;
}
value = apply_param_flags(&value, &flags, state, pf_flags);
let array_join_sep: String = if let Some(ref s) = flags.join_sep {
s.clone()
} else if flags.join_lines {
"\n".to_string()
} else if flags.split_lines {
"\n".to_string()
} else if let Some(ref s) = flags.split_sep {
s.clone()
} else {
" ".to_string()
};
let joined = if flags.join_sep.is_some() || value.len() == 1 {
let sep = flags.join_sep.as_deref().unwrap_or(&array_join_sep);
value.join(sep)
} else if pf_flags & prefork_flags::SHWORDSPLIT != 0 && !qt {
let prefix: String = chars[..dollar_pos].iter().collect();
let suffix: String = chars[pos..].iter().collect();
for (i, v) in value.iter().enumerate() {
if i == 0 && value.len() == 1 {
result_nodes.push(format!("{}{}{}", prefix, v, suffix));
} else if i == 0 {
result_nodes.push(format!("{}{}", prefix, v));
} else if i == value.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(), dollar_pos, result_nodes);
} else {
value.join(&array_join_sep)
};
let prefix: String = chars[..dollar_pos].iter().collect();
let suffix: String = chars[pos..].iter().collect();
let result = format!("{}{}{}", prefix, joined, suffix);
if value.len() > 1 {
result_nodes.extend(value.iter().cloned());
} else {
result_nodes.push(result.clone());
}
(result, prefix.len() + joined.len(), result_nodes)
}
#[derive(Default, Clone, Debug)]
struct ParamFlags {
lowercase: bool,
uppercase: bool,
capitalize: bool,
unique: bool,
sort: bool,
sort_reverse: bool,
sort_array_index: bool,
sort_case_insensitive: bool,
sort_numeric: bool,
keys: bool,
values: bool,
type_info: bool,
prompt_expand: bool,
prompt_percent: bool,
eval: bool,
quote_level: usize,
unquote: bool,
report_error: bool,
split_words: bool,
split_lines: bool,
join_lines: bool,
count_words: bool,
count_words_null: bool,
count_chars: bool,
length_chars: bool,
create_assoc: bool,
array_expand: bool,
glob_subst: bool,
visible: bool,
search: bool,
match_flag: bool,
reverse_subscript: bool,
begin_end_length: bool,
split_sep: Option<String>,
join_sep: Option<String>,
pad_left: Option<usize>,
pad_right: Option<usize>,
pad_char: Option<char>,
pad_string1: Option<String>,
pad_string2: Option<String>,
}
fn get_param_value(name: &str, state: &SubstState) -> String {
state
.variables
.get(name)
.cloned()
.or_else(|| std::env::var(name).ok())
.unwrap_or_default()
}
fn get_param_with_subscript(
name: &str,
subscript: Option<&str>,
state: &SubstState,
) -> Vec<String> {
let parsed_sub = subscript.and_then(parse_subscript_flags);
let (sub_flags, real_sub) = match parsed_sub.as_ref() {
Some((f, s)) => (Some(f), Some(s.as_str())),
None => (None, subscript),
};
if let Some(arr) = state.arrays.get(name) {
if let Some(flags) = sub_flags {
let pat = real_sub.unwrap_or("");
return apply_array_subscript_flags(arr, flags, pat);
}
if let Some(sub) = real_sub {
if sub == "@" || sub == "*" {
return arr.clone();
}
if let Ok(idx) = sub.parse::<i64>() {
let idx = if idx < 0 {
(arr.len() as i64 + idx) as usize
} else {
(idx - 1).max(0) as usize };
return arr.get(idx).cloned().into_iter().collect();
}
}
return arr.clone();
}
if let Some(assoc) = state.assoc_arrays.get(name) {
if let Some(flags) = sub_flags {
let pat = real_sub.unwrap_or("");
let pairs: Vec<(String, String)> = assoc
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
return apply_assoc_subscript_flags(&pairs, flags, pat);
}
if let Some(sub) = real_sub {
if sub == "@" || sub == "*" {
return assoc.values().cloned().collect();
}
return assoc.get(sub).cloned().into_iter().collect();
}
return assoc.values().cloned().collect();
}
if let Some(sub) = real_sub {
if magic_assoc_keys(name, state).is_some()
|| matches!(
name,
"mapfile" | "terminfo" | "termcap" | "errnos" | "sysparams"
)
{
let mut substate_mut = SubstState {
errflag: state.errflag,
opts: state.opts.clone(),
variables: state.variables.clone(),
arrays: state.arrays.clone(),
assoc_arrays: state.assoc_arrays.clone(),
skip_filesub: state.skip_filesub,
function_names: state.function_names.clone(),
command_names: state.command_names.clone(),
alias_names: state.alias_names.clone(),
};
let resolved_sub = singsub_no_tilde(sub, &mut substate_mut);
let val = crate::exec::with_executor(|exec| {
exec.get_special_array_value(name, &resolved_sub).unwrap_or_default()
});
return if val.is_empty() {
Vec::new()
} else {
vec![val]
};
}
}
let value = get_param_value(name, state);
if value.is_empty() {
return Vec::new();
}
if let Some(sub) = real_sub {
let chars: Vec<char> = value.chars().collect();
let n = chars.len() as i64;
let to_idx = |i: i64| -> usize {
if i > 0 {
((i - 1) as usize).min(chars.len())
} else if i < 0 {
((n + i).max(0)) as usize
} else {
0
}
};
if let Some(comma) = sub.find(',') {
if let (Ok(a), Ok(b)) = (
sub[..comma].trim().parse::<i64>(),
sub[comma + 1..].trim().parse::<i64>(),
) {
let start = to_idx(a);
let end = if b > 0 {
(b as usize).min(chars.len())
} else if b < 0 {
((n + b + 1).max(0)) as usize
} else {
0
};
if start < chars.len() && start < end {
let slice: String = chars[start..end.min(chars.len())].iter().collect();
return vec![slice];
}
return Vec::new();
}
}
if let Ok(idx) = sub.parse::<i64>() {
let real = to_idx(idx);
return chars
.get(real)
.map(|c| vec![c.to_string()])
.unwrap_or_default();
}
}
vec![value]
}
#[derive(Default, Debug, Clone, Copy)]
struct SubscriptFlags {
forward_match: bool,
reverse_match: bool,
forward_index: bool,
reverse_index: bool,
exact: bool,
keys: bool,
numeric: bool,
}
fn parse_subscript_flags(sub: &str) -> Option<(SubscriptFlags, String)> {
let s = sub.trim_start();
if !s.starts_with('(') {
return None;
}
let close = s.find(')')?;
let body = &s[1..close];
let rest = &s[close + 1..];
let mut flags = SubscriptFlags::default();
for c in body.chars() {
match c {
'r' => flags.forward_match = true,
'R' => flags.reverse_match = true,
'i' => flags.forward_index = true,
'I' => flags.reverse_index = true,
'e' => flags.exact = true,
'k' => flags.keys = true,
'n' => flags.numeric = true,
_ => return None, }
}
if !flags.forward_match
&& !flags.reverse_match
&& !flags.forward_index
&& !flags.reverse_index
{
return None; }
Some((flags, rest.to_string()))
}
fn apply_array_subscript_flags(
arr: &[String],
flags: &SubscriptFlags,
pat: &str,
) -> Vec<String> {
let matches = |s: &str| -> bool {
if flags.exact {
s == pat
} else if flags.numeric {
s.parse::<f64>().ok() == pat.parse::<f64>().ok()
} else {
let re_src = param_pattern_to_regex(pat);
regex::Regex::new(&re_src)
.map(|re| re.is_match(s))
.unwrap_or(false)
}
};
if flags.forward_match {
arr.iter()
.find(|s| matches(s.as_str()))
.cloned()
.into_iter()
.collect()
} else if flags.reverse_match {
arr.iter()
.rev()
.find(|s| matches(s.as_str()))
.cloned()
.into_iter()
.collect()
} else if flags.forward_index {
let idx = arr.iter().position(|s| matches(s.as_str()));
vec![idx.map(|i| (i + 1).to_string()).unwrap_or_else(|| "0".to_string())]
} else if flags.reverse_index {
let idx = arr.iter().rposition(|s| matches(s.as_str()));
vec![idx.map(|i| (i + 1).to_string()).unwrap_or_else(|| "0".to_string())]
} else {
arr.to_vec()
}
}
fn apply_assoc_subscript_flags(
pairs: &[(String, String)],
flags: &SubscriptFlags,
pat: &str,
) -> Vec<String> {
let matches = |s: &str| -> bool {
if flags.exact {
s == pat
} else {
let re_src = param_pattern_to_regex(pat);
regex::Regex::new(&re_src)
.map(|re| re.is_match(s))
.unwrap_or(false)
}
};
let pick = |entry: &(String, String)| -> String {
if flags.keys {
entry.0.clone()
} else {
entry.1.clone()
}
};
if flags.forward_match {
pairs
.iter()
.find(|e| matches(&pick(e)))
.map(|e| e.1.clone())
.into_iter()
.collect()
} else if flags.reverse_match {
pairs
.iter()
.rev()
.find(|e| matches(&pick(e)))
.map(|e| e.1.clone())
.into_iter()
.collect()
} else if flags.forward_index || flags.reverse_index {
let mut it: Box<dyn Iterator<Item = &(String, String)>> = if flags.reverse_index {
Box::new(pairs.iter().rev())
} else {
Box::new(pairs.iter())
};
it.find(|e| matches(&pick(e)))
.map(|e| e.0.clone())
.into_iter()
.collect()
} else {
pairs.iter().map(|e| e.1.clone()).collect()
}
}
fn apply_param_flags(
value: &[String],
flags: &ParamFlags,
state: &mut SubstState,
pf_flags: u32,
) -> Vec<String> {
let mut result: Vec<String> = value.to_vec();
let ssub = pf_flags & prefork_flags::SINGLE != 0;
if !ssub {
if let Some(ref sep) = flags.split_sep {
let preserve = flags.array_expand;
result = result
.iter()
.flat_map(|s| {
s.split(sep.as_str())
.filter(|f| preserve || !f.is_empty())
.map(String::from)
.collect::<Vec<_>>()
})
.collect();
}
if flags.split_lines {
result = result
.iter()
.flat_map(|s| s.lines().map(String::from))
.collect();
}
if flags.split_words {
result = result
.iter()
.flat_map(|s| z_tokenize(s))
.collect();
}
}
if flags.lowercase {
result = result.iter().map(|s| s.to_lowercase()).collect();
}
if flags.uppercase {
result = result.iter().map(|s| s.to_uppercase()).collect();
}
if flags.capitalize {
result = result
.iter()
.map(|s| {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().chain(chars).collect(),
}
})
.collect();
}
if flags.unique {
let mut seen = std::collections::HashSet::new();
result.retain(|s| seen.insert(s.clone()));
}
if flags.sort {
if flags.sort_numeric {
result.sort_by(|a, b| {
let na: f64 = a.parse().unwrap_or(0.0);
let nb: f64 = b.parse().unwrap_or(0.0);
na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
});
} else if flags.sort_case_insensitive {
result.sort_by_key(|a| a.to_lowercase());
} else {
result.sort();
}
}
if flags.sort_reverse {
result.reverse();
}
match flags.quote_level {
0 => {}
1 => {
result = result
.iter()
.map(|s| {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
' ' | '\t' | '\n' | '\\' | '\'' | '"'
| '`' | '$' | '*' | '?' | '[' | ']'
| '(' | ')' | '{' | '}' | '|' | '&'
| ';' | '<' | '>' | '#' | '~' | '!' => {
out.push('\\');
out.push(c);
}
_ => out.push(c),
}
}
out
})
.collect();
}
2 => {
result = result
.iter()
.map(|s| format!("'{}'", s.replace('\'', "'\\''")))
.collect();
}
3 => {
result = result
.iter()
.map(|s| format!("\"{}\"", s.replace('"', "\\\"").replace('$', "\\$").replace('\\', "\\\\")))
.collect();
}
_ => {
result = result
.iter()
.map(|s| {
let mut out = String::from("$'");
for c in s.chars() {
match c {
'\'' => out.push_str("\\'"),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
_ => out.push(c),
}
}
out.push('\'');
out
})
.collect();
}
}
if flags.unquote {
result = result
.iter()
.map(|s| {
let s = s.trim();
if (s.starts_with('\'') && s.ends_with('\''))
|| (s.starts_with('"') && s.ends_with('"'))
{
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
})
.collect();
}
if flags.eval {
result = result
.iter()
.map(|s| {
let mut list = LinkList::from_string(s);
let mut rf = 0u32;
prefork(&mut list, prefork_flags::NOSHWORDSPLIT, &mut rf, state);
list.get_data(0).unwrap_or("").to_string()
})
.collect();
}
if flags.glob_subst {
}
if flags.report_error && result.iter().all(|s| s.is_empty()) {
state.errflag = true;
}
let _ = flags.array_expand;
if flags.visible {
result = result
.iter()
.map(|s| {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_control() && c != '\n' && c != '\t' {
if (c as u32) < 0x20 {
out.push('^');
out.push(((c as u8) + b'@') as char);
} else {
out.push(c);
}
} else {
out.push(c);
}
}
out
})
.collect();
}
if flags.prompt_percent {
let mut ctx = crate::prompt::PromptContext::default();
if let Some(zero) = state.variables.get("0") {
ctx.scriptname = Some(zero.clone());
}
if let Some(az) = state.variables.get("ZSH_ARGZERO") {
ctx.argzero = az.clone();
}
result = result
.iter()
.map(|s| crate::prompt::expand_prompt(s, &ctx))
.collect();
}
if flags.join_lines {
result = vec![result.join("\n")];
}
if let Some(ref sep) = flags.join_sep {
result = vec![result.join(sep)];
}
if flags.count_words {
let count = result
.iter()
.map(|s| s.split_whitespace().count())
.sum::<usize>();
result = vec![count.to_string()];
}
if flags.count_chars {
let count = result.iter().map(|s| s.chars().count()).sum::<usize>();
result = vec![count.to_string()];
}
if (flags.pad_left.is_some() || flags.pad_right.is_some()) && result.is_empty() {
result = vec![String::new()];
}
if let Some(width) = flags.pad_left {
let s1 = flags.pad_string1.as_deref();
let s2 = flags
.pad_string2
.as_deref()
.or_else(|| flags.pad_char.as_ref().map(|_| ""));
let pre_one = s1;
let pre_mul = match (flags.pad_string1.as_ref(), flags.pad_string2.as_ref()) {
(Some(_), Some(s2)) => Some(s2.as_str()),
(Some(s1), None) => Some(s1.as_str()),
(None, _) => flags.pad_char.as_ref().map(|_| " "),
};
let _ = (s1, s2);
result = result
.iter()
.map(|s| dopadding_simple(s, width, 0, pre_one, pre_mul, None, None, flags.pad_char))
.collect();
}
if let Some(width) = flags.pad_right {
let post_one = flags.pad_string1.as_deref();
let post_mul = match (flags.pad_string1.as_ref(), flags.pad_string2.as_ref()) {
(Some(_), Some(s2)) => Some(s2.as_str()),
(Some(s1), None) => Some(s1.as_str()),
(None, _) => flags.pad_char.as_ref().map(|_| " "),
};
result = result
.iter()
.map(|s| dopadding_simple(s, 0, width, None, None, post_one, post_mul, flags.pad_char))
.collect();
}
result
}
fn strip_outer_dq_markers(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c == DNULL || c == SNULL {
continue;
}
out.push(c);
}
if (out.starts_with('"') && out.ends_with('"') && out.len() >= 2)
|| (out.starts_with('\'') && out.ends_with('\'') && out.len() >= 2)
{
out[1..out.len() - 1].to_string()
} else {
out
}
}
fn apply_operator(
var_name: &str,
subscript: Option<&str>,
value: Vec<String>,
operator: Option<&str>,
operand: &str,
state: &mut SubstState,
) -> Vec<String> {
apply_operator_with_flags(var_name, subscript, value, operator, operand, false, state)
}
fn apply_operator_with_flags(
var_name: &str,
subscript: Option<&str>,
value: Vec<String>,
operator: Option<&str>,
operand: &str,
match_flag: bool,
state: &mut SubstState,
) -> Vec<String> {
let is_set = !value.is_empty();
let is_empty = value.iter().all(|s| s.is_empty());
let joined = value.join(" ");
match operator {
Some(":-") | Some("-") => {
if (operator == Some(":-") && (is_empty || !is_set))
|| (operator == Some("-") && !is_set)
{
let (expanded, _, _, _) =
multsub(operand, prefork_flags::NOSHWORDSPLIT, state);
vec![strip_outer_dq_markers(&expanded)]
} else {
value
}
}
Some(":=") | Some("=") => {
if (operator == Some(":=") && (is_empty || !is_set))
|| (operator == Some("=") && !is_set)
{
let (expanded, _, _, _) =
multsub(operand, prefork_flags::NOSHWORDSPLIT, state);
let val = strip_outer_dq_markers(&expanded);
match subscript {
Some(idx) => {
let is_assoc = state.assoc_arrays.contains_key(var_name);
let numeric = idx.parse::<i64>().ok();
match (is_assoc, numeric) {
(true, _) => {
let map = state
.assoc_arrays
.entry(var_name.to_string())
.or_default();
map.insert(idx.to_string(), val.clone());
}
(false, Some(n)) => {
let arr = state
.arrays
.entry(var_name.to_string())
.or_default();
let pos = if n > 0 { (n - 1) as usize } else { 0 };
if pos >= arr.len() {
arr.resize(pos + 1, String::new());
}
arr[pos] = val.clone();
}
(false, None) => {
let map = state
.assoc_arrays
.entry(var_name.to_string())
.or_default();
map.insert(idx.to_string(), val.clone());
}
}
}
None => {
state
.variables
.insert(var_name.to_string(), val.clone());
}
}
vec![val]
} else {
value
}
}
Some(":+") | Some("+") => {
if (operator == Some(":+") && !is_empty && is_set) || (operator == Some("+") && is_set)
{
let (expanded, _, _, _) =
multsub(operand, prefork_flags::NOSHWORDSPLIT, state);
vec![strip_outer_dq_markers(&expanded)]
} else {
vec![]
}
}
Some(":?") | Some("?") => {
if (operator == Some(":?") && (is_empty || !is_set))
|| (operator == Some("?") && !is_set)
{
let msg = if operand.is_empty() {
format!("{}: parameter not set", var_name)
} else {
operand.to_string()
};
eprintln!("{}", msg);
state.errflag = true;
vec![]
} else {
value
}
}
Some(":#") => {
let regex_src = param_pattern_to_regex(operand);
let re_opt = regex::Regex::new(®ex_src).ok();
value
.into_iter()
.filter_map(|s| {
let matches = re_opt
.as_ref()
.map(|re| re.is_match(&s))
.unwrap_or(false);
let keep = if match_flag { matches } else { !matches };
if keep {
Some(s)
} else if match_flag {
None
} else {
None
}
})
.collect::<Vec<_>>()
}
Some("::=") => {
let (expanded, _, _, _) =
multsub(operand, prefork_flags::NOSHWORDSPLIT, state);
let val = strip_outer_dq_markers(&expanded);
match subscript {
Some(idx) => {
let is_assoc = state.assoc_arrays.contains_key(var_name);
let numeric = idx.parse::<i64>().ok();
match (is_assoc, numeric) {
(true, _) => {
let map = state
.assoc_arrays
.entry(var_name.to_string())
.or_default();
map.insert(idx.to_string(), val.clone());
}
(false, Some(n)) => {
let arr = state
.arrays
.entry(var_name.to_string())
.or_default();
let pos = if n > 0 { (n - 1) as usize } else { 0 };
if pos >= arr.len() {
arr.resize(pos + 1, String::new());
}
arr[pos] = val.clone();
}
(false, None) => {
let map = state
.assoc_arrays
.entry(var_name.to_string())
.or_default();
map.insert(idx.to_string(), val.clone());
}
}
}
None => {
state
.variables
.insert(var_name.to_string(), val.clone());
}
}
vec![val]
}
Some(":mod") => {
let chain = format!(":{}", operand);
value
.iter()
.map(|s| modify(s, &chain, state))
.collect()
}
Some(":") => {
let parts: Vec<&str> = operand.split(':').collect();
let offset: i64 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let length: Option<i64> = parts.get(1).and_then(|s| s.parse().ok());
let is_array_ref = matches!(var_name, "@" | "*" | "argv")
|| (state.arrays.contains_key(var_name) && value.len() > 1);
if is_array_ref {
let arr = &value;
let n = arr.len() as i64;
let start = if offset < 0 {
((n + offset).max(0)) as usize
} else if offset == 0 {
0
} else {
((offset - 1).max(0) as usize).min(arr.len())
};
let end = match length {
Some(l) if l < 0 => ((n + l).max(start as i64)) as usize,
Some(l) => (start + l as usize).min(arr.len()),
None => arr.len(),
};
return arr[start..end].to_vec();
}
value
.iter()
.map(|s| {
let chars: Vec<char> = s.chars().collect();
let len = chars.len() as i64;
let start = if offset < 0 {
(len + offset).max(0) as usize
} else {
(offset as usize).min(chars.len())
};
let end = match length {
Some(l) if l < 0 => (len + l).max(start as i64) as usize,
Some(l) => (start + l as usize).min(chars.len()),
None => chars.len(),
};
chars[start..end].iter().collect()
})
.collect()
}
Some("#") => {
value
.iter()
.map(|s| strip_prefix_match(s, operand, false, match_flag))
.collect()
}
Some("##") => {
value
.iter()
.map(|s| strip_prefix_match(s, operand, true, match_flag))
.collect()
}
Some("%") => {
value
.iter()
.map(|s| strip_suffix_match(s, operand, false, match_flag))
.collect()
}
Some("%%") => {
value
.iter()
.map(|s| strip_suffix_match(s, operand, true, match_flag))
.collect()
}
Some("/") | Some("//") | Some("/#") | Some("/%") => {
let chars: Vec<char> = operand.chars().collect();
let mut sep_idx: Option<usize> = None;
let mut k = 0;
while k < chars.len() {
if chars[k] == '\\' && k + 1 < chars.len() {
k += 2;
continue;
}
if chars[k] == '/' {
sep_idx = Some(k);
break;
}
k += 1;
}
let unesc_slash = |s: &str| -> String {
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(c);
}
out
};
let (raw_pat, raw_rep_owned): (String, String) = match sep_idx {
Some(p) => (
unesc_slash(&chars[..p].iter().collect::<String>()),
unesc_slash(&chars[p + 1..].iter().collect::<String>()),
),
None => (unesc_slash(operand), String::new()),
};
let (pat_no_flags, backref_mode, _case_i) = strip_inline_pattern_flags(&raw_pat);
let pattern = singsub_no_tilde(&pat_no_flags, state);
let pattern = pattern.replace('\x00', "");
let regex_src = if backref_mode {
glob_to_regex_capturing(&pattern, false)
} else {
param_pattern_to_regex_anchored(&pattern, false)
};
let re_opt = regex::Regex::new(®ex_src).ok();
let op_str = operator.unwrap_or("/").to_string();
let mut out_vals: Vec<String> = Vec::with_capacity(value.len());
for s in value.iter() {
out_vals.push(do_replace_one(
s,
&op_str,
&pattern,
&raw_rep_owned,
re_opt.as_ref(),
backref_mode,
state,
));
}
out_vals
}
Some(":^") | Some(":^^") => {
let second = state
.arrays
.get(operand)
.cloned()
.or_else(|| state.variables.get(operand).map(|v| vec![v.clone()]))
.unwrap_or_default();
if value.is_empty() || second.is_empty() {
return value;
}
let long = operator == Some(":^^");
let total = if long {
value.len().max(second.len())
} else {
value.len().min(second.len())
};
let mut out: Vec<String> = Vec::with_capacity(total * 2);
for i in 0..total {
let a = &value[i % value.len()];
let b = &second[i % second.len()];
out.push(a.clone());
out.push(b.clone());
}
out
}
Some("@op") => {
eprintln!("zshrs:1: bad substitution");
state.errflag = true;
Vec::new()
}
Some("^") => {
eprintln!("zshrs:1: bad substitution");
state.errflag = true;
Vec::new()
}
Some("^^") => {
eprintln!("zshrs:1: bad substitution");
state.errflag = true;
Vec::new()
}
Some(",") => {
eprintln!("zshrs:1: bad substitution");
state.errflag = true;
Vec::new()
}
Some(",,") => {
eprintln!("zshrs:1: bad substitution");
state.errflag = true;
Vec::new()
}
_ => value,
}
}
fn find_prefix_match(s: &str, pattern: &str) -> Vec<usize> {
let chars: Vec<char> = s.chars().collect();
let mut matches: Vec<usize> = Vec::new();
for end in 0..=chars.len() {
let candidate: String = chars[..end].iter().collect();
if crate::glob::pattern_match(pattern, &candidate, false, true) {
matches.push(end);
}
}
matches
}
fn find_suffix_match(s: &str, pattern: &str) -> Vec<usize> {
let chars: Vec<char> = s.chars().collect();
let mut matches: Vec<usize> = Vec::new();
for start in 0..=chars.len() {
let candidate: String = chars[start..].iter().collect();
if crate::glob::pattern_match(pattern, &candidate, false, true) {
matches.push(start);
}
}
matches
}
fn strip_prefix_match(s: &str, pattern: &str, greedy: bool, match_flag: bool) -> String {
let chars: Vec<char> = s.chars().collect();
let matches = find_prefix_match(s, pattern);
let chosen = if greedy {
matches.iter().copied().max()
} else {
matches.iter().copied().min()
};
match chosen {
Some(end) => {
if match_flag {
chars[..end].iter().collect()
} else {
chars[end..].iter().collect()
}
}
None => {
if match_flag {
String::new()
} else {
s.to_string()
}
}
}
}
fn strip_suffix_match(s: &str, pattern: &str, greedy: bool, match_flag: bool) -> String {
let chars: Vec<char> = s.chars().collect();
let matches = find_suffix_match(s, pattern);
let chosen = if greedy {
matches.iter().copied().min()
} else {
matches.iter().copied().max()
};
match chosen {
Some(start) => {
if match_flag {
chars[start..].iter().collect()
} else {
chars[..start].iter().collect()
}
}
None => {
if match_flag {
String::new()
} else {
s.to_string()
}
}
}
}
fn remove_prefix(s: &str, pattern: &str, greedy: bool) -> String {
if pattern == "*" {
return String::new();
}
if let Some(prefix) = pattern.strip_suffix('*') {
if let Some(rest) = s.strip_prefix(prefix) {
if greedy {
if let Some(i) = (prefix.len()..=s.len()).next_back() {
return s[i..].to_string();
}
} else {
return rest.to_string();
}
}
} else if let Some(rest) = s.strip_prefix(pattern) {
return rest.to_string();
}
s.to_string()
}
fn remove_suffix(s: &str, pattern: &str, greedy: bool) -> String {
if pattern == "*" {
return String::new();
}
if let Some(suffix) = pattern.strip_prefix('*') {
if let Some(prefix) = s.strip_suffix(suffix) {
if greedy {
if let Some(i) = (0..=s.len().saturating_sub(suffix.len())).next() {
return s[..i].to_string();
}
} else {
return prefix.to_string();
}
}
} else if let Some(prefix) = s.strip_suffix(pattern) {
return prefix.to_string();
}
s.to_string()
}
fn split_words(s: &str, state: &SubstState) -> Vec<String> {
let ifs = state
.variables
.get("IFS")
.map(|s| s.as_str())
.unwrap_or(" \t\n");
s.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
fn find_matching_bracket(s: &str, open: char, close: char) -> Option<usize> {
let mut depth = 1;
for (i, c) in s.chars().enumerate() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}
fn find_matching_parmath(s: &str) -> Option<usize> {
let mut depth = 1;
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == INPARMATH {
depth += 1;
} else if chars[i] == OUTPARMATH {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
i += 1;
}
None
}
fn hasbraces(s: &str) -> bool {
let bytes: &[u8] = s.as_bytes();
let n = bytes.len();
let mut i = 0;
while i < n {
let c = bytes[i];
if c == b'$' && i + 1 < n && bytes[i + 1] == b'{' {
let mut depth = 1;
i += 2;
while i < n && depth > 0 {
match bytes[i] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
i += 1;
}
continue;
}
if c == b'\\' {
i += 2;
continue;
}
if c == b'{' {
let mut depth = 1;
let mut j = i + 1;
let mut comma_found = false;
let mut range_found = false;
while j < n && depth > 0 {
match bytes[j] {
b'\\' => {
j += 2;
continue;
}
b'$' if j + 1 < n && bytes[j + 1] == b'{' => {
j += 2;
let mut nd = 1;
while j < n && nd > 0 {
match bytes[j] {
b'{' => nd += 1,
b'}' => nd -= 1,
_ => {}
}
j += 1;
}
continue;
}
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
j += 1;
break;
}
}
b',' if depth == 1 => comma_found = true,
b'.' if depth == 1
&& j + 1 < n
&& bytes[j + 1] == b'.' =>
{
range_found = true;
j += 1; }
_ => {}
}
j += 1;
}
if depth == 0 && (comma_found || range_found) {
return true;
}
i = j;
continue;
}
i += 1;
}
false
}
fn xpandbraces(list: &mut LinkList, node_idx: &mut usize) {
let data = match list.get_data(*node_idx) {
Some(d) => d.to_string(),
None => return,
};
let bytes = data.as_bytes();
let n = bytes.len();
let mut i = 0;
while i < n {
let c = bytes[i];
if c == b'$' && i + 1 < n && bytes[i + 1] == b'{' {
let mut depth = 1;
i += 2;
while i < n && depth > 0 {
match bytes[i] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
i += 1;
}
continue;
}
if c == b'{' {
let start = i;
let mut depth = 1;
let mut j = i + 1;
while j < n && depth > 0 {
if bytes[j] == b'$' && j + 1 < n && bytes[j + 1] == b'{' {
let mut nd = 1;
j += 2;
while j < n && nd > 0 {
match bytes[j] {
b'{' => nd += 1,
b'}' => nd -= 1,
_ => {}
}
j += 1;
}
continue;
}
match bytes[j] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if depth != 0 {
i += 1;
continue;
}
let end = j; let prefix = &data[..start];
let content = &data[start + 1..end];
let suffix = &data[end + 1..];
if let Some(rng_pos) = content.find("..") {
let left = &content[..rng_pos];
let rest = &content[rng_pos + 2..];
let (right, step_str) = match rest.find("..") {
Some(p) => (&rest[..p], Some(&rest[p + 2..])),
None => (rest, None),
};
if let (Ok(a), Ok(b)) = (left.trim().parse::<i64>(), right.trim().parse::<i64>())
{
let step = step_str
.and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(1)
.abs()
.max(1);
let mut nodes_added: Vec<String> = Vec::new();
if a <= b {
let mut k = a;
while k <= b {
nodes_added.push(format!("{}{}{}", prefix, k, suffix));
k += step;
}
} else {
let mut k = a;
while k >= b {
nodes_added.push(format!("{}{}{}", prefix, k, suffix));
k -= step;
}
}
list.remove(*node_idx);
for (k, item) in nodes_added.into_iter().enumerate() {
if k == 0 {
list.nodes.insert(*node_idx, LinkNode { data: item });
} else {
list.insert_after(*node_idx + k - 1, item);
}
}
return;
}
let lc: Vec<char> = left.chars().collect();
let rc: Vec<char> = right.chars().collect();
if lc.len() == 1 && rc.len() == 1 {
let a = lc[0];
let b = rc[0];
let mut nodes_added: Vec<String> = Vec::new();
if a <= b {
for c in (a as u32)..=(b as u32) {
if let Some(ch) = char::from_u32(c) {
nodes_added.push(format!("{}{}{}", prefix, ch, suffix));
}
}
} else {
for c in ((b as u32)..=(a as u32)).rev() {
if let Some(ch) = char::from_u32(c) {
nodes_added.push(format!("{}{}{}", prefix, ch, suffix));
}
}
}
list.remove(*node_idx);
for (k, item) in nodes_added.into_iter().enumerate() {
if k == 0 {
list.nodes.insert(*node_idx, LinkNode { data: item });
} else {
list.insert_after(*node_idx + k - 1, item);
}
}
return;
}
}
let mut alts: Vec<String> = Vec::new();
let mut depth_c = 0;
let mut current = String::new();
for c in content.chars() {
match c {
'{' => {
depth_c += 1;
current.push(c);
}
'}' => {
depth_c -= 1;
current.push(c);
}
',' if depth_c == 0 => {
alts.push(std::mem::take(&mut current));
}
_ => current.push(c),
}
}
alts.push(current);
if alts.len() > 1 {
list.remove(*node_idx);
for (k, alt) in alts.iter().enumerate() {
let expanded = format!("{}{}{}", prefix, alt, suffix);
if k == 0 {
list.nodes.insert(*node_idx, LinkNode { data: expanded });
} else {
list.insert_after(*node_idx + k - 1, expanded);
}
}
return;
}
i = end + 1;
continue;
}
i += 1;
}
}
fn remnulargs(s: &str) -> String {
s.chars().filter(|&c| c != NULARG).collect()
}
fn filesub(s: &str, _flags: u32, _state: &mut SubstState) -> String {
if let Some(rest) = s.strip_prefix('~') {
let (user, suffix) = match rest.find('/') {
Some(pos) => (&rest[..pos], &rest[pos..]),
None => (rest, ""),
};
if user.is_empty() {
if let Ok(home) = std::env::var("HOME") {
return format!("{}{}", home, suffix);
}
} else if user == "+" {
if let Ok(pwd) = std::env::var("PWD") {
return format!("{}{}", pwd, suffix);
}
} else if user == "-" {
if let Ok(oldpwd) = std::env::var("OLDPWD") {
return format!("{}{}", oldpwd, suffix);
}
}
}
if s.starts_with('=') && s.len() > 1 {
let cmd = &s[1..];
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
let full_path = format!("{}/{}", dir, cmd);
if std::path::Path::new(&full_path).exists() {
return full_path;
}
}
}
}
s.to_string()
}
fn getproc(s: &str, state: &mut SubstState) -> (Option<String>, String) {
let chars: Vec<char> = s.chars().collect();
let is_input = chars[0] == INANG;
if let Some(end) = find_matching_bracket(&s[1..], INPAR, OUTPAR) {
let cmd: String = s[2..end + 1].chars().collect();
let rest = s[end + 2..].to_string();
if state.opts.exec_opt {
let fd = if is_input { "63" } else { "62" };
return (Some(format!("/dev/fd/{}", fd)), rest);
}
return (None, rest);
}
(None, s.to_string())
}
fn getoutputfile(s: &str, state: &mut SubstState) -> (Option<String>, String) {
if let Some(end) = find_matching_bracket(&s[1..], INPAR, OUTPAR) {
let cmd: String = s[2..end + 1].chars().collect();
let rest = s[end + 2..].to_string();
if state.opts.exec_opt {
let output = run_command(&cmd);
return (Some("/tmp/zsh_proc_subst".to_string()), rest);
}
return (None, rest);
}
(None, s.to_string())
}
fn arithsubst(expr: &str, _state: &mut SubstState) -> String {
match crate::math::matheval(expr) {
Ok(crate::math::MathNum::Integer(n)) => n.to_string(),
Ok(crate::math::MathNum::Float(f)) => f.to_string(),
Ok(crate::math::MathNum::Unset) | Err(_) => "0".to_string(),
}
}
fn run_command(cmd: &str) -> String {
use std::process::{Command, Stdio};
match Command::new("sh")
.arg("-c")
.arg(cmd)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()
{
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
Err(_) => String::new(),
}
}
pub mod multsub_flags {
pub const WS_AT_START: u32 = 1;
pub const WS_AT_END: u32 = 2;
pub const PARAM_NAME: u32 = 4;
}
pub fn singsub(s: &str, state: &mut SubstState) -> String {
let mut list = LinkList::from_string(s);
let mut ret_flags = 0u32;
prefork(&mut list, prefork_flags::SINGLE, &mut ret_flags, state);
if state.errflag {
return String::new();
}
list.get_data(0).unwrap_or("").to_string()
}
pub fn singsub_no_tilde(s: &str, state: &mut SubstState) -> String {
let saved = state.skip_filesub;
state.skip_filesub = true;
let result = singsub(s, state);
state.skip_filesub = saved;
result
}
pub fn multsub(s: &str, pf_flags: u32, state: &mut SubstState) -> (String, Vec<String>, bool, u32) {
let mut x = s.to_string();
let mut ms_flags = 0u32;
if pf_flags & prefork_flags::SPLIT != 0 {
let leading_ws: String = x.chars().take_while(|c| c.is_ascii_whitespace()).collect();
if !leading_ws.is_empty() {
ms_flags |= multsub_flags::WS_AT_START;
x = x.chars().skip(leading_ws.len()).collect();
}
}
let mut list = LinkList::from_string(&x);
if pf_flags & prefork_flags::SPLIT != 0 {
let mut node_idx = 0;
let mut in_quote = false;
let mut in_paren = 0;
while node_idx < list.len() {
if let Some(data) = list.get_data(node_idx) {
let chars: Vec<char> = data.chars().collect();
let mut split_points = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'"' | '\'' | TICK | QTICK => in_quote = !in_quote,
INPAR => in_paren += 1,
OUTPAR => in_paren = (in_paren - 1).max(0),
_ => {}
}
if !in_quote && in_paren == 0 {
let ifs = state
.variables
.get("IFS")
.map(|s| s.as_str())
.unwrap_or(" \t\n");
if ifs.contains(c) && !is_token(c) {
split_points.push(i);
}
}
i += 1;
}
if !split_points.is_empty() {
let data_str = data.to_string();
let chars: Vec<char> = data_str.chars().collect();
let mut last = 0;
list.remove(node_idx);
for (idx, &point) in split_points.iter().enumerate() {
if point > last {
let segment: String = chars[last..point].iter().collect();
if idx == 0 {
list.nodes.insert(node_idx, LinkNode { data: segment });
} else {
list.insert_after(node_idx + idx - 1, segment);
}
}
last = point + 1;
}
if last < chars.len() {
let segment: String = chars[last..].iter().collect();
if split_points.is_empty() {
list.nodes.insert(node_idx, LinkNode { data: segment });
} else {
list.insert_after(node_idx + split_points.len() - 1, segment);
}
}
}
}
node_idx += 1;
}
}
let mut ret_flags = 0u32;
prefork(&mut list, pf_flags, &mut ret_flags, state);
if state.errflag {
return (String::new(), Vec::new(), false, ms_flags);
}
if pf_flags & prefork_flags::SPLIT != 0 {
if let Some(last) = list.nodes.back() {
if last
.data
.chars()
.last()
.map(|c| c.is_ascii_whitespace())
.unwrap_or(false)
{
ms_flags |= multsub_flags::WS_AT_END;
}
}
}
let len = list.len();
if len > 1 || (list.flags & LF_ARRAY != 0) {
let arr: Vec<String> = list.nodes.iter().map(|n| n.data.clone()).collect();
let joined = arr.join(" ");
return (joined, arr, true, ms_flags);
}
let result = list.get_data(0).unwrap_or("").to_string();
(result.clone(), vec![result], false, ms_flags)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CaseMod {
None,
Lower,
Upper,
Caps,
}
pub fn casemodify(s: &str, casmod: CaseMod) -> String {
match casmod {
CaseMod::None => s.to_string(),
CaseMod::Lower => s.to_lowercase(),
CaseMod::Upper => s.to_uppercase(),
CaseMod::Caps => {
let mut result = String::new();
let mut nextupper = true;
for c in s.chars() {
if !c.is_alphanumeric() {
nextupper = true;
result.push(c);
} else if nextupper {
result.extend(c.to_uppercase());
nextupper = false;
} else {
result.extend(c.to_lowercase());
}
}
result
}
}
}
pub fn modify(s: &str, modifiers: &str, state: &mut SubstState) -> String {
let mut result = s.to_string();
let mut chars: std::iter::Peekable<std::str::Chars> = modifiers.chars().peekable();
let mut last_subst: Option<(String, String)> = None;
while chars.peek() == Some(&':') {
chars.next();
let mut gbal = false;
let mut wall = false;
let mut sep: Option<String> = None;
loop {
match chars.peek() {
Some(&'g') => {
gbal = true;
chars.next();
}
Some(&'w') => {
wall = true;
chars.next();
}
Some(&'W') => {
chars.next();
if chars.peek() == Some(&':') {
chars.next();
let collected: String =
chars.by_ref().take_while(|&c| c != ':').collect();
sep = Some(collected);
}
}
_ => break,
}
}
let modifier = match chars.next() {
Some(c) => c,
None => break,
};
if modifier == 's' || modifier == 'S' {
let delim = match chars.next() {
Some(c) => c,
None => break,
};
let pat: String = chars.by_ref().take_while(|&c| c != delim).collect();
let repl: String = chars.by_ref().take_while(|&c| c != delim).collect();
result = apply_subst(&result, &pat, &repl, gbal);
last_subst = Some((pat, repl));
continue;
}
if modifier == '&' {
if let Some((p, r)) = &last_subst {
result = apply_subst(&result, p, r, gbal);
}
continue;
}
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 apply_single_modifier(w, modifier, gbal, state) {
Some(m) => modified.push(m),
None => {
eprintln!("zshrs: unrecognized modifier `{}'", modifier);
state.errflag = true;
return String::new();
}
}
}
result = modified.join(separator);
} else {
match apply_single_modifier(&result, modifier, gbal, state) {
Some(m) => result = m,
None => {
eprintln!("zshrs: unrecognized modifier `{}'", modifier);
state.errflag = true;
return String::new();
}
}
}
}
result
}
fn apply_subst(s: &str, pat: &str, repl: &str, gbal: bool) -> String {
if pat.is_empty() {
return s.to_string();
}
if gbal {
s.replace(pat, repl)
} else {
match s.find(pat) {
Some(i) => format!("{}{}{}", &s[..i], repl, &s[i + pat.len()..]),
None => s.to_string(),
}
}
}
fn apply_single_modifier(
s: &str,
modifier: char,
gbal: bool,
_state: &mut SubstState,
) -> Option<String> {
Some(match modifier {
'a' => lexical_canonicalize(s, false),
'A' => {
let lex = lexical_canonicalize(s, false);
match std::fs::canonicalize(&lex) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => lex,
}
}
'c' => {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
let full = format!("{}/{}", dir, s);
if std::path::Path::new(&full).exists() {
return Some(full);
}
}
}
s.to_string()
}
'h' => remtpath(s, 0),
't' => {
let trimmed = s.trim_end_matches('/');
match trimmed.rfind('/') {
Some(pos) => trimmed[pos + 1..].to_string(),
None => trimmed.to_string(),
}
}
'r' => match s.rfind('.') {
Some(pos) if pos > 0 && !s[..pos].ends_with('/') => s[..pos].to_string(),
_ => s.to_string(),
},
'e' => match s.rfind('.') {
Some(pos) if pos > 0 && !s[..pos].ends_with('/') => s[pos + 1..].to_string(),
_ => String::new(),
},
'l' => s.to_lowercase(),
'u' => s.to_uppercase(),
'q' => quote_backslash(s),
'Q' => unquote_subst(s),
'P' => {
let path = if s.starts_with('/') {
s.to_string()
} else if let Ok(cwd) = std::env::current_dir() {
format!("{}/{}", cwd.display(), s)
} else {
s.to_string()
};
match std::fs::canonicalize(&path) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => path,
}
}
_ => return None,
})
}
#[allow(clippy::too_many_arguments)]
fn dopadding_simple(
s: &str,
pre_num: usize,
post_num: usize,
pre_one: Option<&str>,
pre_mul: Option<&str>,
post_one: Option<&str>,
post_mul: Option<&str>,
fallback_char: Option<char>,
) -> String {
if pre_num > 0
&& pre_one.map(|s| s.is_empty()).unwrap_or(false)
&& pre_mul.map(|s| !s.is_empty()).unwrap_or(false)
{
let fill = pre_mul.unwrap();
let mut out = String::with_capacity(pre_num);
let fill_chars: Vec<char> = fill.chars().collect();
for i in 0..pre_num {
out.push(fill_chars[i % fill_chars.len()]);
}
return out;
}
if post_num > 0
&& post_one.map(|s| s.is_empty()).unwrap_or(false)
&& post_mul.map(|s| !s.is_empty()).unwrap_or(false)
{
let fill = post_mul.unwrap();
let mut out = String::with_capacity(post_num);
let fill_chars: Vec<char> = fill.chars().collect();
for i in 0..post_num {
out.push(fill_chars[i % fill_chars.len()]);
}
return out;
}
let value_chars: Vec<char> = s.chars().collect();
let value_len = value_chars.len();
if pre_num > 0 {
if value_len >= pre_num {
return value_chars[value_len - pre_num..].iter().collect();
}
let need = pre_num - value_len;
let one = pre_one.unwrap_or("");
let one_chars: Vec<char> = one.chars().collect();
let one_take = one_chars.len().min(need);
let mul_need = need - one_take;
let fill_str = pre_mul
.map(|s| s.to_string())
.or_else(|| fallback_char.map(|c| c.to_string()))
.unwrap_or_else(|| " ".to_string());
let fill_chars: Vec<char> = fill_str.chars().collect();
let mut out = String::with_capacity(pre_num + value_len);
if !fill_chars.is_empty() {
for i in 0..mul_need {
out.push(fill_chars[i % fill_chars.len()]);
}
} else {
for _ in 0..mul_need {
out.push(' ');
}
}
let take_from = one_chars.len().saturating_sub(one_take);
for c in &one_chars[take_from..] {
out.push(*c);
}
out.extend(value_chars);
return out;
}
if post_num > 0 {
if value_len >= post_num {
return value_chars[..post_num].iter().collect();
}
let need = post_num - value_len;
let one = post_one.unwrap_or("");
let one_chars: Vec<char> = one.chars().collect();
let one_take = one_chars.len().min(need);
let mul_need = need - one_take;
let fill_str = post_mul
.map(|s| s.to_string())
.or_else(|| fallback_char.map(|c| c.to_string()))
.unwrap_or_else(|| " ".to_string());
let fill_chars: Vec<char> = fill_str.chars().collect();
let mut out: String = value_chars.iter().collect();
for c in &one_chars[..one_take] {
out.push(*c);
}
if !fill_chars.is_empty() {
for i in 0..mul_need {
out.push(fill_chars[i % fill_chars.len()]);
}
} else {
for _ in 0..mul_need {
out.push(' ');
}
}
return out;
}
s.to_string()
}
fn z_tokenize(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
let push = |out: &mut Vec<String>, cur: &mut String| {
if !cur.is_empty() {
out.push(std::mem::take(cur));
}
};
let is_meta = |c: char| matches!(c, ';' | '&' | '|' | '<' | '>' | '(' | ')');
let mut cur = String::new();
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
push(&mut out, &mut cur);
i += 1;
continue;
}
if c == '\'' {
cur.push(c);
i += 1;
while i < chars.len() && chars[i] != '\'' {
cur.push(chars[i]);
i += 1;
}
if i < chars.len() {
cur.push(chars[i]);
i += 1;
}
continue;
}
if c == '"' {
cur.push(c);
i += 1;
while i < chars.len() && chars[i] != '"' {
if chars[i] == '\\' && i + 1 < chars.len() {
cur.push(chars[i]);
cur.push(chars[i + 1]);
i += 2;
continue;
}
cur.push(chars[i]);
i += 1;
}
if i < chars.len() {
cur.push(chars[i]);
i += 1;
}
continue;
}
if c == '\\' && i + 1 < chars.len() {
cur.push(c);
cur.push(chars[i + 1]);
i += 2;
continue;
}
if is_meta(c) {
push(&mut out, &mut cur);
let mut tok = String::from(c);
if i + 1 < chars.len() {
let pair = (c, chars[i + 1]);
let combined = matches!(
pair,
('&', '&')
| ('|', '|')
| (';', ';')
| ('>', '>')
| ('<', '<')
| ('>', '&')
| ('<', '&')
);
if combined {
tok.push(chars[i + 1]);
i += 2;
out.push(tok);
continue;
}
}
out.push(tok);
i += 1;
continue;
}
cur.push(c);
i += 1;
}
push(&mut out, &mut cur);
out
}
fn apply_chained_subscript(value: Vec<String>, sub: &str, state: &mut SubstState) -> Vec<String> {
let resolved_sub = singsub_no_tilde(sub, state);
let s = resolved_sub.trim();
if s.is_empty() {
return value;
}
if s == "@" || s == "*" {
return value;
}
if let Some(comma) = s.find(',') {
if let (Ok(a), Ok(b)) = (
s[..comma].trim().parse::<i64>(),
s[comma + 1..].trim().parse::<i64>(),
) {
if value.len() == 1 {
let chars: Vec<char> = value[0].chars().collect();
let n = chars.len() as i64;
let start = if a > 0 { (a - 1) as usize } else if a < 0 { ((n + a).max(0)) as usize } else { 0 };
let end = if b > 0 { (b as usize).min(chars.len()) } else if b < 0 { ((n + b + 1).max(0)) as usize } else { 0 };
if start <= end && start <= chars.len() {
return vec![chars[start..end.min(chars.len())].iter().collect()];
}
return vec![String::new()];
} else {
let n = value.len() as i64;
let start = if a > 0 { (a - 1) as usize } else if a < 0 { ((n + a).max(0)) as usize } else { 0 };
let end = if b > 0 { (b as usize).min(value.len()) } else if b < 0 { ((n + b + 1).max(0)) as usize } else { 0 };
if start < value.len() && start <= end {
return value[start..end.min(value.len())].to_vec();
}
return Vec::new();
}
}
}
if let Ok(idx) = s.parse::<i64>() {
if value.len() == 1 {
let chars: Vec<char> = value[0].chars().collect();
let n = chars.len() as i64;
let real = if idx > 0 {
(idx - 1) as usize
} else if idx < 0 {
let off = n + idx;
if off < 0 {
return vec![String::new()];
}
off as usize
} else {
return vec![String::new()];
};
return chars
.get(real)
.map(|c| vec![c.to_string()])
.unwrap_or_else(|| vec![String::new()]);
}
let n = value.len() as i64;
let real = if idx > 0 {
(idx - 1) as usize
} else if idx < 0 {
let off = n + idx;
if off < 0 {
return Vec::new();
}
off as usize
} else {
return Vec::new();
};
return value.into_iter().nth(real).map(|v| vec![v]).unwrap_or_default();
}
value
}
fn quote_backslash(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
' ' | '\t' | '\n' | '\'' | '"' | '`' | '\\' | '$' | '&' | '|' | ';'
| '<' | '>' | '(' | ')' | '{' | '}' | '[' | ']' | '*' | '?' | '!'
| '#' | '~' | '^' | '=' => {
out.push('\\');
out.push(c);
}
_ => out.push(c),
}
}
out
}
fn unquote_subst(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
if let Some(&nx) = chars.peek() {
out.push(nx);
chars.next();
}
}
'\'' => {
while let Some(&inner) = chars.peek() {
chars.next();
if inner == '\'' {
break;
}
out.push(inner);
}
}
'"' => {
while let Some(&inner) = chars.peek() {
chars.next();
if inner == '"' {
break;
}
if inner == '\\' {
if let Some(&esc) = chars.peek() {
out.push(esc);
chars.next();
continue;
}
}
out.push(inner);
}
}
_ => out.push(c),
}
}
out
}
fn lexical_canonicalize(s: &str, keep_relative: bool) -> String {
if s.is_empty() {
return s.to_string();
}
let absolute = s.starts_with('/');
let base: String = if absolute || keep_relative {
s.to_string()
} else {
let cwd = std::env::var("PWD")
.or_else(|_| {
std::env::current_dir().map(|p| p.to_string_lossy().to_string())
})
.unwrap_or_default();
if cwd.is_empty() {
s.to_string()
} else {
format!("{}/{}", cwd.trim_end_matches('/'), s)
}
};
let mut out: Vec<&str> = Vec::new();
for seg in base.split('/') {
match seg {
"" | "." => continue,
".." => {
out.pop();
}
_ => out.push(seg),
}
}
let leading = if base.starts_with('/') { "/" } else { "" };
if out.is_empty() {
if leading.is_empty() { ".".to_string() } else { "/".to_string() }
} else {
format!("{}{}", leading, out.join("/"))
}
}
pub fn wcpadwidth(wc: char, multi_width: i32) -> i32 {
match multi_width {
0 => 1,
1 => {
let w = char_display_width(wc);
if w >= 0 { w } else { 0 }
}
_ => if char_display_width(wc) > 0 { 1 } else { 0 },
}
}
fn char_display_width(c: char) -> i32 {
let cp = c as u32;
if cp < 0x20 || (0x7F..0xA0).contains(&cp) {
return 0;
}
if (0x0300..=0x036F).contains(&cp)
|| (0x0483..=0x0489).contains(&cp)
|| (0x0591..=0x05BD).contains(&cp)
|| (0x0610..=0x061A).contains(&cp)
|| (0x064B..=0x065F).contains(&cp)
|| (0x0670..=0x0670).contains(&cp)
|| (0x06D6..=0x06DC).contains(&cp)
|| cp == 0x200B
|| cp == 0x200C
|| cp == 0x200D
|| cp == 0xFEFF
{
return 0;
}
if (0x1100..=0x115F).contains(&cp)
|| (0x2E80..=0x303E).contains(&cp)
|| (0x3041..=0x33FF).contains(&cp)
|| (0x3400..=0x4DBF).contains(&cp)
|| (0x4E00..=0x9FFF).contains(&cp)
|| (0xA000..=0xA4CF).contains(&cp)
|| (0xAC00..=0xD7A3).contains(&cp)
|| (0xF900..=0xFAFF).contains(&cp)
|| (0xFE30..=0xFE4F).contains(&cp)
|| (0xFF00..=0xFF60).contains(&cp)
|| (0xFFE0..=0xFFE6).contains(&cp)
|| (0x20000..=0x2FFFD).contains(&cp)
|| (0x30000..=0x3FFFD).contains(&cp)
{
return 2;
}
1
}
pub fn subst_parse_str(s: &str, single: bool, err: bool) -> Option<String> {
let mut buf: String = s.to_string();
if !single {
let mut chars: Vec<char> = buf.chars().collect();
let mut qt = false;
let dnull: char = '\u{91}';
for c in chars.iter_mut() {
if !qt {
if *c == '\u{8c}' {
*c = '\u{85}';
} else if *c == '\u{8e}' {
*c = '\u{84}';
}
}
if *c == dnull {
qt = !qt;
}
}
buf = chars.iter().collect();
}
if err {
let mut depth_dq = 0usize;
let mut depth_sq = 0usize;
for c in buf.chars() {
if c == '"' {
depth_dq ^= 1;
} else if c == '\'' {
depth_sq ^= 1;
}
}
if depth_dq != 0 || depth_sq != 0 {
return None;
}
}
Some(buf)
}
pub fn dstackent(ch: char, val: i32, dirstack: &[String], pwd: &str) -> Option<String> {
let backwards = ch == '-';
if !backwards && val == 0 {
return Some(pwd.to_string());
}
let idx = if backwards {
dirstack.len().checked_sub(val as usize)?
} else {
(val - 1) as usize
};
dirstack.get(idx).cloned()
}
pub fn subst(s: &str, old: &str, new: &str, global: bool) -> String {
if global {
s.replace(old, new)
} else {
s.replacen(old, new, 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum QuoteType {
None,
Backslash,
BackslashPattern,
Single,
Double,
Dollars,
QuotedZputs,
SingleOptional,
}
pub fn quotestring(s: &str, qt: QuoteType) -> String {
match qt {
QuoteType::None => s.to_string(),
QuoteType::Backslash | QuoteType::BackslashPattern => {
let mut result = String::new();
for c in s.chars() {
match c {
' ' | '\t' | '\n' | '\\' | '\'' | '"' | '$' | '`' | '!' | '*' | '?' | '['
| ']' | '(' | ')' | '{' | '}' | '<' | '>' | '|' | '&' | ';' | '#' | '~' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
result
}
QuoteType::Single => {
format!("'{}'", s.replace('\'', "'\\''"))
}
QuoteType::Double => {
let mut result = String::from("\"");
for c in s.chars() {
match c {
'"' | '\\' | '$' | '`' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
result.push('"');
result
}
QuoteType::Dollars => {
let mut result = String::from("$'");
for c in s.chars() {
match c {
'\'' => result.push_str("\\'"),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\t' => result.push_str("\\t"),
'\r' => result.push_str("\\r"),
c if c.is_ascii_control() => {
result.push_str(&format!("\\x{:02x}", c as u32));
}
_ => result.push(c),
}
}
result.push('\'');
result
}
QuoteType::QuotedZputs | QuoteType::SingleOptional => {
let needs_quote = s.chars().any(|c| {
matches!(
c,
' ' | '\t'
| '\n'
| '\\'
| '\''
| '"'
| '$'
| '`'
| '!'
| '*'
| '?'
| '['
| ']'
| '('
| ')'
| '{'
| '}'
| '<'
| '>'
| '|'
| '&'
| ';'
| '#'
| '~'
)
});
if needs_quote {
format!("'{}'", s.replace('\'', "'\\''"))
} else {
s.to_string()
}
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SortOptions {
pub somehow: bool,
pub backwards: bool,
pub case_insensitive: bool,
pub numeric: bool,
pub numeric_signed: bool,
}
pub fn sort_array(arr: &mut [String], opts: &SortOptions) {
if !opts.somehow {
return;
}
if opts.numeric || opts.numeric_signed {
arr.sort_by(|a, b| {
let na: f64 = a.parse().unwrap_or(0.0);
let nb: f64 = b.parse().unwrap_or(0.0);
na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
});
} else if opts.case_insensitive {
arr.sort_by_key(|a| a.to_lowercase());
} else {
arr.sort();
}
if opts.backwards {
arr.reverse();
}
}
pub fn wordcount(s: &str, sep: Option<&str>, count_empty: bool) -> usize {
let separator = sep.unwrap_or(" \t\n");
if count_empty {
s.split(|c: char| separator.contains(c)).count()
} else {
s.split(|c: char| separator.contains(c))
.filter(|w| !w.is_empty())
.count()
}
}
pub fn sepjoin(arr: &[String], sep: Option<&str>, use_ifs_first: bool) -> String {
let separator = sep.unwrap_or(if use_ifs_first { " " } else { "" });
arr.join(separator)
}
pub fn sepsplit(s: &str, sep: Option<&str>, allow_empty: bool, _handle_ifs: bool) -> Vec<String> {
let separator = sep.unwrap_or(" \t\n");
if allow_empty {
s.split(|c: char| separator.contains(c))
.map(String::from)
.collect()
} else {
s.split(|c: char| separator.contains(c))
.filter(|w| !w.is_empty())
.map(String::from)
.collect()
}
}
pub fn unique_array(arr: &mut Vec<String>) {
let mut seen = std::collections::HashSet::new();
arr.retain(|s| seen.insert(s.clone()));
}
pub fn dopadding(
s: &str,
prenum: usize,
postnum: usize,
preone: Option<&str>,
postone: Option<&str>,
premul: &str,
postmul: &str,
) -> String {
let len = s.chars().count();
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 = result.chars().count();
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 chars = s.chars().peekable();
let del = chars.next()?;
let close_del = match del {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
INPAR => OUTPAR,
INBRACK => OUTBRACK,
INBRACE => OUTBRACE,
INANG => OUTANG,
_ => del,
};
let mut content = String::new();
let mut rest_start = 1;
for (i, c) in s.chars().enumerate().skip(1) {
if c == close_del {
rest_start = i + 1;
break;
}
content.push(c);
rest_start = i + 1;
}
let rest = &s[rest_start.min(s.len())..];
Some((del, content, rest))
}
pub fn get_intarg(s: &str) -> Option<(i64, &str)> {
if let Some((_, content, rest)) = get_strarg(s) {
let val: i64 = content.trim().parse().ok()?;
Some((val.abs(), rest))
} else {
None
}
}
pub fn substnamedir(s: &str) -> String {
if let Ok(home) = std::env::var("HOME") {
if s.starts_with(&home) {
return format!("~{}", &s[home.len()..]);
}
}
s.to_string()
}
pub fn nicedupstring(s: &str) -> String {
let mut result = String::new();
for c in s.chars() {
if c.is_ascii_control() {
match c {
'\n' => result.push_str("\\n"),
'\t' => result.push_str("\\t"),
'\r' => result.push_str("\\r"),
_ => result.push_str(&format!("\\x{:02x}", c as u32)),
}
} else {
result.push(c);
}
}
result
}
pub fn untokenize(s: &str) -> String {
s.chars().map(token_to_char).collect()
}
pub fn shtokenize(s: &str) -> String {
let mut result = String::new();
for c in s.chars() {
match c {
'*' => result.push('\u{91}'), '?' => result.push('\u{92}'), '[' => result.push(INBRACK),
']' => result.push(OUTBRACK),
_ => result.push(c),
}
}
result
}
pub fn check_subst_complete(s: &str) -> bool {
let mut depth = 0;
let mut in_brace = 0;
for c in s.chars() {
match c {
INPAR => depth += 1,
OUTPAR => depth -= 1,
INBRACE | '{' => in_brace += 1,
OUTBRACE | '}' => in_brace -= 1,
_ => {}
}
}
depth == 0 && in_brace == 0
}
pub fn quotesubst(s: &str, state: &mut SubstState) -> String {
let mut result = s.to_string();
let mut pos = 0;
while pos < result.len() {
let chars: Vec<char> = result.chars().collect();
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;
}
}
remnulargs(&result)
}
pub fn globlist(list: &mut LinkList, flags: u32, state: &mut SubstState) {
let mut node_idx = 0;
while node_idx < list.len() && !state.errflag {
if let Some(data) = list.get_data(node_idx) {
if flags & prefork_flags::KEY_VALUE != 0 && data.starts_with(MARKER) {
node_idx += 3;
continue;
}
let expanded = zglob(data, flags & prefork_flags::NO_UNTOK != 0, state);
if expanded.is_empty() {
if state.opts.glob_subst {
}
} else if expanded.len() == 1 {
list.set_data(node_idx, expanded[0].clone());
} else {
list.remove(node_idx);
for (i, path) in expanded.iter().enumerate() {
if i == 0 {
list.nodes.insert(node_idx, LinkNode { data: path.clone() });
} else {
list.insert_after(node_idx + i - 1, path.clone());
}
}
node_idx += expanded.len();
continue;
}
}
node_idx += 1;
}
}
fn zglob(pattern: &str, no_untok: bool, state: &SubstState) -> Vec<String> {
let pattern = if no_untok {
pattern.to_string()
} else {
untokenize(pattern)
};
if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
if std::path::Path::new(&pattern).exists() {
return vec![pattern];
}
return vec![pattern];
}
match glob::glob(&pattern) {
Ok(paths) => {
let matches: Vec<String> = paths
.filter_map(|p| p.ok())
.map(|p| p.to_string_lossy().to_string())
.collect();
if matches.is_empty() {
vec![pattern]
} else {
matches
}
}
Err(_) => vec![pattern],
}
}
pub fn skipparens(s: &str, open: char, close: char) -> Option<usize> {
let mut depth = 1;
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}
pub fn getoutput(cmd: &str, qt: bool, state: &mut SubstState) -> Option<Vec<String>> {
if !state.opts.exec_opt {
return Some(vec![]);
}
let output = run_command(cmd);
let output = output.trim_end_matches('\n');
if qt {
Some(vec![output.to_string()])
} else {
Some(output.lines().map(String::from).collect())
}
}
pub fn parse_subscript(s: &str, _allow_range: bool) -> Option<(String, String)> {
let chars: Vec<char> = s.chars().collect();
if chars.first() != Some(&'[') && chars.first() != Some(&INBRACK) {
return None;
}
let mut depth = 1;
let mut end = 1;
while end < chars.len() && depth > 0 {
let c = chars[end];
if c == '[' || c == INBRACK {
depth += 1;
} else if c == ']' || c == OUTBRACK {
depth -= 1;
}
if depth > 0 {
end += 1;
}
}
if depth != 0 {
return None;
}
let subscript: String = chars[1..end].iter().collect();
let rest_start = end + 1;
let rest = if rest_start < s.len() {
s[rest_start..].to_string()
} else {
String::new()
};
Some((subscript, rest))
}
pub fn eval_subscript(subscript: &str, array_len: usize) -> (usize, Option<usize>) {
if let Some(comma_pos) = subscript.find(',') {
let start_str = subscript[..comma_pos].trim();
let end_str = subscript[comma_pos + 1..].trim();
let start = parse_index(start_str, array_len);
let end = parse_index(end_str, array_len);
(start, Some(end))
} else {
let idx = parse_index(subscript.trim(), array_len);
(idx, None)
}
}
fn parse_index(s: &str, array_len: usize) -> usize {
if let Ok(idx) = s.parse::<i64>() {
if idx < 0 {
let abs_idx = (-idx) as usize;
array_len.saturating_sub(abs_idx)
} else if idx == 0 {
0
} else {
(idx as usize).saturating_sub(1)
}
} else {
0
}
}
pub fn itok(c: char) -> bool {
let code = c as u32;
(0x80..=0x9F).contains(&code)
}
pub fn ztokens(c: char) -> char {
match c {
POUND => '#',
STRING => '$',
QSTRING => '$',
TICK => '`',
QTICK => '`',
INPAR => '(',
OUTPAR => ')',
INBRACE => '{',
OUTBRACE => '}',
INBRACK => '[',
OUTBRACK => ']',
INANG => '<',
OUTANG => '>',
EQUALS => '=',
_ => c,
}
}
pub mod sub_flags {
pub const END: u32 = 1; pub const LONG: u32 = 2; pub const SUBSTR: u32 = 4; pub const MATCH: u32 = 8; pub const REST: u32 = 16; pub const BIND: u32 = 32; pub const EIND: u32 = 64; pub const LEN: u32 = 128; pub const ALL: u32 = 256; pub const GLOBAL: u32 = 512; pub const START: u32 = 1024; pub const EGLOB: u32 = 2048; }
pub fn getmatch(val: &str, pattern: &str, flags: u32, flnum: i32, replstr: Option<&str>) -> String {
let val_chars: Vec<char> = val.chars().collect();
let val_len = val_chars.len();
let regex_pattern = glob_to_regex(pattern);
match regex::Regex::new(®ex_pattern) {
Ok(re) => {
if flags & sub_flags::GLOBAL != 0 {
let replacement = replstr.unwrap_or("");
re.replace_all(val, replacement).to_string()
} else if flags & sub_flags::END != 0 {
if flags & sub_flags::LONG != 0 {
for i in 0..=val_len {
let suffix: String = val_chars[i..].iter().collect();
if re.is_match(&suffix) {
let prefix: String = val_chars[..i].iter().collect();
return if let Some(repl) = replstr {
format!("{}{}", prefix, repl)
} else {
prefix
};
}
}
} else {
for i in (0..=val_len).rev() {
let suffix: String = val_chars[i..].iter().collect();
if re.is_match(&suffix) {
let prefix: String = val_chars[..i].iter().collect();
return if let Some(repl) = replstr {
format!("{}{}", prefix, repl)
} else {
prefix
};
}
}
}
val.to_string()
} else {
if flags & sub_flags::LONG != 0 {
for i in (0..=val_len).rev() {
let prefix: String = val_chars[..i].iter().collect();
if re.is_match(&prefix) {
let suffix: String = val_chars[i..].iter().collect();
return if let Some(repl) = replstr {
format!("{}{}", repl, suffix)
} else {
suffix
};
}
}
} else {
for i in 0..=val_len {
let prefix: String = val_chars[..i].iter().collect();
if re.is_match(&prefix) {
let suffix: String = val_chars[i..].iter().collect();
return if let Some(repl) = replstr {
format!("{}{}", repl, suffix)
} else {
suffix
};
}
}
}
val.to_string()
}
}
Err(_) => {
if let Some(repl) = replstr {
val.replace(pattern, repl)
} else {
val.to_string()
}
}
}
}
fn strip_inline_pattern_flags(pat: &str) -> (String, bool, bool) {
if !pat.starts_with("(#") {
return (pat.to_string(), false, false);
}
let after = &pat[2..];
let close = match after.find(')') {
Some(i) => i,
None => return (pat.to_string(), false, false),
};
let flag_str = &after[..close];
let rest = &after[close + 1..];
let mut backref = false;
let mut case_i = false;
for c in flag_str.chars() {
match c {
'b' => backref = true,
'B' => backref = false,
'i' => case_i = true,
'I' => case_i = false,
'l' => {} _ => return (pat.to_string(), false, false),
}
}
(rest.to_string(), backref, case_i)
}
fn glob_to_regex_capturing(pattern: &str, anchored: bool) -> String {
let mut regex = String::new();
if anchored {
regex.push('^');
}
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
let consume_postfix = |chars: &[char], i: &mut usize| -> Option<&'static str> {
if *i + 1 < chars.len() && chars[*i + 1] == '#' {
if *i + 2 < chars.len() && chars[*i + 2] == '#' {
*i += 2;
Some("+")
} else {
*i += 1;
Some("*")
}
} else {
None
}
};
while i < chars.len() {
match chars[i] {
'*' => {
regex.push_str(".*");
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
'?' => {
regex.push('.');
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
'[' => {
regex.push('[');
i += 1;
if i < chars.len() && (chars[i] == '!' || chars[i] == '^') {
regex.push('^');
i += 1;
}
while i < chars.len() && chars[i] != ']' {
if chars[i] == '\\' && i + 1 < chars.len() {
regex.push('\\');
regex.push(chars[i + 1]);
i += 2;
} else {
regex.push(chars[i]);
i += 1;
}
}
regex.push(']');
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
'\\' if i + 4 < chars.len()
&& chars[i + 1] == '('
&& chars[i + 2] == '#'
&& (chars[i + 3] == 'e' || chars[i + 3] == 's')
&& chars[i + 4] == ')' =>
{
regex.push_str("\\\\");
regex.push(if chars[i + 3] == 'e' { '$' } else { '^' });
i += 4;
}
'\\' if i + 1 < chars.len() => {
regex.push(chars[i + 1]);
i += 1;
}
'(' if i + 3 < chars.len()
&& chars[i + 1] == '#'
&& (chars[i + 2] == 'e' || chars[i + 2] == 's')
&& chars[i + 3] == ')' =>
{
regex.push(if chars[i + 2] == 'e' { '$' } else { '^' });
i += 3; }
'(' | '|' => regex.push(chars[i]),
')' => {
regex.push(')');
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
c @ ('.' | '+' | '^' | '$' | '{' | '}') => {
regex.push('\\');
regex.push(c);
}
c => {
regex.push(c);
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
}
i += 1;
}
if anchored {
regex.push('$');
}
regex
}
fn populate_match_array(caps: ®ex::Captures, state: &mut SubstState) {
let mut arr = Vec::with_capacity(caps.len());
for i in 1..caps.len() {
arr.push(caps.get(i).map(|m| m.as_str().to_string()).unwrap_or_default());
}
state.arrays.insert("match".to_string(), arr);
}
#[allow(clippy::too_many_arguments)]
fn do_replace_one(
s: &str,
op: &str,
pattern_lit: &str,
raw_rep: &str,
re_opt: Option<®ex::Regex>,
backref_mode: bool,
state: &mut SubstState,
) -> String {
match (re_opt, op) {
(Some(rx), "/") => {
if let Some(caps) = rx.captures(s) {
let m = caps.get(0).unwrap();
if backref_mode {
populate_match_array(&caps, state);
}
let r = singsub_no_tilde(raw_rep, state);
return format!("{}{}{}", &s[..m.start()], r, &s[m.end()..]);
}
s.to_string()
}
(Some(rx), "//") => {
let mut out = String::with_capacity(s.len());
let mut last = 0usize;
for caps in rx.captures_iter(s) {
let m = caps.get(0).unwrap();
out.push_str(&s[last..m.start()]);
if backref_mode {
populate_match_array(&caps, state);
}
let r = singsub_no_tilde(raw_rep, state);
out.push_str(&r);
last = m.end();
}
out.push_str(&s[last..]);
out
}
(Some(rx), "/#") => {
if let Some(caps) = rx.captures(s) {
let m = caps.get(0).unwrap();
if m.start() == 0 {
if backref_mode {
populate_match_array(&caps, state);
}
let r = singsub_no_tilde(raw_rep, state);
return format!("{}{}", r, &s[m.end()..]);
}
}
s.to_string()
}
(Some(rx), "/%") => {
let mut last_caps: Option<regex::Captures> = None;
for caps in rx.captures_iter(s) {
if caps.get(0).unwrap().end() == s.len() {
last_caps = Some(caps);
}
}
if let Some(caps) = last_caps {
let m = caps.get(0).unwrap();
if backref_mode {
populate_match_array(&caps, state);
}
let r = singsub_no_tilde(raw_rep, state);
return format!("{}{}", &s[..m.start()], r);
}
s.to_string()
}
_ => {
let replacement = singsub_no_tilde(raw_rep, state);
match op {
"/" => s.replacen(pattern_lit, &replacement, 1),
"//" => s.replace(pattern_lit, &replacement),
"/#" => match s.strip_prefix(pattern_lit) {
Some(rest) => format!("{}{}", replacement, rest),
None => s.to_string(),
},
"/%" => match s.strip_suffix(pattern_lit) {
Some(head) => format!("{}{}", head, replacement),
None => s.to_string(),
},
_ => s.to_string(),
}
}
}
}
fn param_pattern_to_regex_anchored(pattern: &str, anchored: bool) -> String {
let mut regex = String::new();
if anchored {
regex.push('^');
}
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
let consume_postfix = |chars: &[char], i: &mut usize| -> Option<&'static str> {
if *i + 1 < chars.len() && chars[*i + 1] == '#' {
if *i + 2 < chars.len() && chars[*i + 2] == '#' {
*i += 2;
Some("+")
} else {
*i += 1;
Some("*")
}
} else {
None
}
};
while i < chars.len() {
match chars[i] {
'*' => {
regex.push_str(".*");
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
'?' => {
regex.push('.');
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
'[' => {
regex.push('[');
i += 1;
if i < chars.len() && (chars[i] == '!' || chars[i] == '^') {
regex.push('^');
i += 1;
}
while i < chars.len() && chars[i] != ']' {
if chars[i] == '\\' && i + 1 < chars.len() {
regex.push('\\');
regex.push(chars[i + 1]);
i += 2;
} else {
regex.push(chars[i]);
i += 1;
}
}
regex.push(']');
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
'\\' if i + 1 < chars.len() => {
regex.push('\\');
regex.push(chars[i + 1]);
i += 1;
}
c @ ('.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}') => {
regex.push('\\');
regex.push(c);
}
c => {
regex.push(c);
if let Some(q) = consume_postfix(&chars, &mut i) {
regex.push_str(q);
}
}
}
i += 1;
}
if anchored {
regex.push('$');
}
regex
}
fn param_pattern_to_regex(pattern: &str) -> String {
param_pattern_to_regex_anchored(pattern, true)
}
pub fn compile_glob_to_regex_for_replace(pattern: &str) -> String {
param_pattern_to_regex_anchored(pattern, false)
}
fn glob_to_regex(pattern: &str) -> String {
let mut regex = String::from("^");
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'*' => {
if i + 1 < chars.len() && chars[i + 1] == '*' {
regex.push_str(".*");
i += 1;
} else {
regex.push_str("[^/]*");
}
}
'?' => regex.push('.'),
'[' => {
regex.push('[');
i += 1;
if i < chars.len() && (chars[i] == '!' || chars[i] == '^') {
regex.push('^');
i += 1;
}
while i < chars.len() && chars[i] != ']' {
if chars[i] == '\\' && i + 1 < chars.len() {
regex.push('\\');
i += 1;
regex.push(chars[i]);
} else {
regex.push(chars[i]);
}
i += 1;
}
regex.push(']');
}
'.' | '+' | '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
regex.push('\\');
regex.push(chars[i]);
}
c if itok(c) => {
regex.push(ztokens(c));
}
c => regex.push(c),
}
i += 1;
}
regex.push('$');
regex
}
pub fn getmatcharr(
aval: &mut [String],
pattern: &str,
flags: u32,
flnum: i32,
replstr: Option<&str>,
) {
for val in aval.iter_mut() {
*val = getmatch(val, pattern, flags, flnum, replstr);
}
}
pub fn array_union(arr1: &[String], arr2: &[String]) -> Vec<String> {
let set2: std::collections::HashSet<_> = arr2.iter().collect();
arr1.iter().filter(|s| !set2.contains(s)).cloned().collect()
}
pub fn array_intersection(arr1: &[String], arr2: &[String]) -> Vec<String> {
let set2: std::collections::HashSet<_> = arr2.iter().collect();
arr1.iter().filter(|s| set2.contains(s)).cloned().collect()
}
pub fn array_zip(arr1: &[String], arr2: &[String], shortest: bool) -> Vec<String> {
let len = if shortest {
arr1.len().min(arr2.len())
} else {
arr1.len().max(arr2.len())
};
let mut result = Vec::with_capacity(len * 2);
for i in 0..len {
let idx1 = if arr1.is_empty() { 0 } else { i % arr1.len() };
let idx2 = if arr2.is_empty() { 0 } else { i % arr2.len() };
result.push(arr1.get(idx1).cloned().unwrap_or_default());
result.push(arr2.get(idx2).cloned().unwrap_or_default());
}
result
}
pub fn strcatsub(prefix: &str, src: &str, suffix: &str, glob_subst: bool) -> String {
let mut result = String::with_capacity(prefix.len() + src.len() + suffix.len());
result.push_str(prefix);
if glob_subst {
result.push_str(&shtokenize(src));
} else {
result.push_str(src);
}
result.push_str(suffix);
result
}
pub fn inull(c: char) -> bool {
matches!(c, '\u{8F}' | '\u{94}' | '\u{95}' | '\u{92}')
}
pub fn chuck(s: &str, pos: usize) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if i != pos {
result.push(c);
}
}
result
}
pub fn getsparam(name: &str, state: &SubstState) -> Option<String> {
if let Some(val) = state.variables.get(name) {
return Some(val.clone());
}
std::env::var(name).ok()
}
pub fn getaparam(name: &str, state: &SubstState) -> Option<Vec<String>> {
state.arrays.get(name).cloned()
}
pub fn gethparam(
name: &str,
state: &SubstState,
) -> Option<indexmap::IndexMap<String, String>> {
state.assoc_arrays.get(name).cloned()
}
pub fn setsparam(name: &str, value: &str, state: &mut SubstState) {
state.variables.insert(name.to_string(), value.to_string());
}
pub fn setaparam(name: &str, value: Vec<String>, state: &mut SubstState) {
state.arrays.insert(name.to_string(), value);
}
pub fn sethparam(
name: &str,
value: indexmap::IndexMap<String, String>,
state: &mut SubstState,
) {
state.assoc_arrays.insert(name.to_string(), value);
}
pub fn hmkarray(val: &str) -> Vec<String> {
if val.is_empty() {
Vec::new()
} else {
vec![val.to_string()]
}
}
pub fn dupstrpfx(s: &str, len: usize) -> String {
s.chars().take(len).collect()
}
pub fn dyncat(s1: &str, s2: &str) -> String {
format!("{}{}", s1, s2)
}
pub fn zhtricat(s1: &str, s2: &str, s3: &str) -> String {
format!("{}{}{}", s1, s2, s3)
}
pub fn findword(s: &str, sep: Option<&str>) -> Option<(String, String)> {
let separator = sep.unwrap_or(" \t\n");
let trimmed = s.trim_start_matches(|c: char| separator.contains(c));
if trimmed.is_empty() {
return None;
}
let word_end = trimmed
.find(|c: char| separator.contains(c))
.unwrap_or(trimmed.len());
let word = &trimmed[..word_end];
let rest = &trimmed[word_end..];
Some((word.to_string(), rest.to_string()))
}
pub fn is_absolute_path(s: &str) -> bool {
s.starts_with('/')
}
pub fn remtpath(s: &str, count: usize) -> String {
let bytes: Vec<u8> = s.bytes().collect();
let n = bytes.len();
if n == 0 {
return s.to_string();
}
let is_sep = |b: u8| b == b'/';
let mut end: isize = (n as isize) - 1;
while end >= 0 && is_sep(bytes[end as usize]) {
end -= 1;
}
if count == 0 {
while end >= 0 && !is_sep(bytes[end as usize]) {
end -= 1;
}
if end < 0 {
return if is_sep(bytes[0]) {
"/".to_string()
} else {
".".to_string()
};
}
while end > 0 && is_sep(bytes[(end - 1) as usize]) {
end -= 1;
}
if end == 0 {
end += 1;
if (end as usize) < n
&& is_sep(bytes[end as usize])
&& (end + 1 >= n as isize || !is_sep(bytes[(end + 1) as usize]))
{
end += 1;
}
}
return s[..end as usize].to_string();
}
let mut strp: usize = 0;
let mut remaining = count as isize;
let limit = end as usize;
while strp < limit {
if is_sep(bytes[strp]) {
remaining -= 1;
if remaining <= 0 {
if strp == 0 {
strp += 1;
}
return s[..strp].to_string();
}
while strp + 1 < bytes.len() && is_sep(bytes[strp + 1]) {
strp += 1;
}
}
strp += 1;
}
s.to_string()
}
pub fn remlpaths(s: &str, count: usize) -> String {
if s.is_empty() || count == 0 {
return s.to_string();
}
let bytes: &[u8] = s.as_bytes();
let mut end = bytes.len();
while end > 0 && bytes[end - 1] == b'/' {
end -= 1;
}
if end == 0 {
return s.to_string();
}
let mut count = count as isize;
let mut i: isize = (end as isize) - 1;
loop {
while i >= 0 {
if bytes[i as usize] == b'/' {
count -= 1;
if count > 0 {
if i > 0 {
i -= 1;
break; } else {
return s[..end].to_string();
}
}
return s[(i as usize + 1)..end].to_string();
}
i -= 1;
}
while i >= 0 && bytes[i as usize] == b'/' {
i -= 1;
}
if i <= 0 {
break;
}
}
s[..end].to_string()
}
pub fn remtext(s: &str) -> String {
if let Some(pos) = s.rfind('.') {
if let Some(slash_pos) = s.rfind('/') {
if pos > slash_pos {
return s[..pos].to_string();
}
} else {
return s[..pos].to_string();
}
}
s.to_string()
}
pub fn rembutext(s: &str) -> String {
if let Some(pos) = s.rfind('.') {
if let Some(slash_pos) = s.rfind('/') {
if pos > slash_pos {
return s[pos + 1..].to_string();
}
} else {
return s[pos + 1..].to_string();
}
}
String::new()
}
pub fn chabspath(s: &str) -> String {
let abs = if s.starts_with('/') {
s.to_string()
} else if let Ok(cwd) = std::env::current_dir() {
format!("{}/{}", cwd.display(), s)
} else {
s.to_string()
};
let mut out: Vec<&str> = Vec::new();
for seg in abs.split('/') {
match seg {
"" | "." => continue, ".." => {
out.pop();
}
other => out.push(other),
}
}
if out.is_empty() {
return "/".to_string();
}
let mut result = String::new();
for seg in &out {
result.push('/');
result.push_str(seg);
}
result
}
pub fn chrealpath(s: &str) -> String {
match std::fs::canonicalize(s) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => s.to_string(),
}
}
pub fn xsymlink(path: &str, resolve: bool) -> String {
if resolve {
match std::fs::canonicalize(path) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => path.to_string(),
}
} else {
path.to_string()
}
}
pub fn convbase(val: i64, base: u32, underscore: bool) -> String {
if base == 10 {
if underscore {
let s = val.abs().to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.insert(0, '_');
}
result.insert(0, c);
}
if val < 0 {
result.insert(0, '-');
}
result
} else {
val.to_string()
}
} else if base == 16 {
format!("{:x}", val)
} else if base == 8 {
format!("{:o}", val)
} else if base == 2 {
format!("{:b}", val)
} else {
val.to_string()
}
}
pub fn matheval(expr: &str) -> MathResult {
if let Ok(n) = expr.trim().parse::<i64>() {
return MathResult::Integer(n);
}
if let Ok(n) = expr.trim().parse::<f64>() {
return MathResult::Float(n);
}
let expr = expr.trim();
if let Some(pos) = expr.rfind('+') {
if pos > 0 {
let left = matheval(&expr[..pos]);
let right = matheval(&expr[pos + 1..]);
return match (left, right) {
(MathResult::Integer(a), MathResult::Integer(b)) => MathResult::Integer(a + b),
(MathResult::Float(a), MathResult::Float(b)) => MathResult::Float(a + b),
(MathResult::Integer(a), MathResult::Float(b)) => MathResult::Float(a as f64 + b),
(MathResult::Float(a), MathResult::Integer(b)) => MathResult::Float(a + b as f64),
};
}
}
if let Some(pos) = expr.rfind('-') {
if pos > 0 {
let left = matheval(&expr[..pos]);
let right = matheval(&expr[pos + 1..]);
return match (left, right) {
(MathResult::Integer(a), MathResult::Integer(b)) => MathResult::Integer(a - b),
(MathResult::Float(a), MathResult::Float(b)) => MathResult::Float(a - b),
(MathResult::Integer(a), MathResult::Float(b)) => MathResult::Float(a as f64 - b),
(MathResult::Float(a), MathResult::Integer(b)) => MathResult::Float(a - b as f64),
};
}
}
if let Some(pos) = expr.rfind('*') {
let left = matheval(&expr[..pos]);
let right = matheval(&expr[pos + 1..]);
return match (left, right) {
(MathResult::Integer(a), MathResult::Integer(b)) => MathResult::Integer(a * b),
(MathResult::Float(a), MathResult::Float(b)) => MathResult::Float(a * b),
(MathResult::Integer(a), MathResult::Float(b)) => MathResult::Float(a as f64 * b),
(MathResult::Float(a), MathResult::Integer(b)) => MathResult::Float(a * b as f64),
};
}
if let Some(pos) = expr.rfind('/') {
let left = matheval(&expr[..pos]);
let right = matheval(&expr[pos + 1..]);
return match (left, right) {
(MathResult::Integer(a), MathResult::Integer(b)) if b != 0 => {
MathResult::Integer(a / b)
}
(MathResult::Float(a), MathResult::Float(b)) => MathResult::Float(a / b),
(MathResult::Integer(a), MathResult::Float(b)) => MathResult::Float(a as f64 / b),
(MathResult::Float(a), MathResult::Integer(b)) => MathResult::Float(a / b as f64),
_ => MathResult::Integer(0),
};
}
if let Some(pos) = expr.rfind('%') {
let left = matheval(&expr[..pos]);
let right = matheval(&expr[pos + 1..]);
return match (left, right) {
(MathResult::Integer(a), MathResult::Integer(b)) if b != 0 => {
MathResult::Integer(a % b)
}
_ => MathResult::Integer(0),
};
}
MathResult::Integer(0)
}
#[derive(Debug, Clone, Copy)]
pub enum MathResult {
Integer(i64),
Float(f64),
}
impl std::fmt::Display for MathResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MathResult::Integer(n) => write!(f, "{}", n),
MathResult::Float(n) => write!(f, "{}", n),
}
}
}
impl MathResult {
pub fn to_i64(&self) -> i64 {
match self {
MathResult::Integer(n) => *n,
MathResult::Float(n) => *n as i64,
}
}
}
pub fn mathevali(expr: &str) -> i64 {
matheval(expr).to_i64()
}
pub fn parse_subst_string(s: &str) -> Result<String, String> {
Ok(s.to_string())
}
pub fn bufferwords(s: &str, flags: u32) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let mut in_quote = false;
let mut quote_char = '\0';
let mut escape_next = false;
for c in s.chars() {
if escape_next {
current.push(c);
escape_next = false;
continue;
}
match c {
'\\' => {
escape_next = true;
current.push(c);
}
'"' | '\'' => {
if in_quote && c == quote_char {
in_quote = false;
quote_char = '\0';
} else if !in_quote {
in_quote = true;
quote_char = c;
}
current.push(c);
}
' ' | '\t' | '\n' if !in_quote => {
if !current.is_empty() {
words.push(current.clone());
current.clear();
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
words.push(current);
}
words
}
pub mod scanpm_flags {
pub const WANTKEYS: u32 = 1;
pub const WANTVALS: u32 = 2;
pub const MATCHKEY: u32 = 4;
pub const MATCHVAL: u32 = 8;
pub const KEYMATCH: u32 = 16;
pub const DQUOTED: u32 = 32;
pub const ARRONLY: u32 = 64;
pub const CHECKING: u32 = 128;
pub const NOEXEC: u32 = 256;
pub const ISVAR_AT: u32 = 512;
pub const ASSIGNING: u32 = 1024;
pub const WANTINDEX: u32 = 2048;
pub const NONAMESPC: u32 = 4096;
pub const NONAMEREF: u32 = 8192;
}
pub fn fetchvalue(
name: &str,
subscript: Option<&str>,
flags: u32,
state: &SubstState,
) -> Option<ParamValue> {
if let Some(arr) = state.arrays.get(name) {
if let Some(sub) = subscript {
if sub == "@" || sub == "*" {
return Some(ParamValue::Array(arr.clone()));
}
let (idx, end_idx) = eval_subscript(sub, arr.len());
if let Some(end) = end_idx {
let slice: Vec<String> = arr.get(idx..=end).map(|s| s.to_vec()).unwrap_or_default();
return Some(ParamValue::Array(slice));
} else if idx < arr.len() {
return Some(ParamValue::Scalar(arr[idx].clone()));
}
}
return Some(ParamValue::Array(arr.clone()));
}
if let Some(hash) = state.assoc_arrays.get(name) {
if let Some(sub) = subscript {
if sub == "@" || sub == "*" {
if flags & scanpm_flags::WANTKEYS != 0 {
return Some(ParamValue::Array(hash.keys().cloned().collect()));
} else {
return Some(ParamValue::Array(hash.values().cloned().collect()));
}
}
if let Some(val) = hash.get(sub) {
return Some(ParamValue::Scalar(val.clone()));
}
}
return Some(ParamValue::Array(hash.values().cloned().collect()));
}
if let Some(val) = state.variables.get(name) {
return Some(ParamValue::Scalar(val.clone()));
}
if let Ok(val) = std::env::var(name) {
return Some(ParamValue::Scalar(val));
}
None
}
#[derive(Debug, Clone)]
pub enum ParamValue {
Scalar(String),
Array(Vec<String>),
}
impl Default for ParamValue {
fn default() -> Self {
ParamValue::Scalar(String::new())
}
}
impl std::fmt::Display for ParamValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParamValue::Scalar(s) => f.write_str(s),
ParamValue::Array(arr) => f.write_str(&arr.join(" ")),
}
}
}
impl ParamValue {
pub fn to_array(&self) -> Vec<String> {
match self {
ParamValue::Scalar(s) => vec![s.clone()],
ParamValue::Array(arr) => arr.clone(),
}
}
pub fn is_array(&self) -> bool {
matches!(self, ParamValue::Array(_))
}
}
pub fn getstrvalue(pv: &ParamValue) -> String {
pv.to_string()
}
pub fn getarrvalue(pv: &ParamValue) -> Vec<String> {
pv.to_array()
}
pub fn arrlen(arr: &[String]) -> usize {
arr.len()
}
pub fn arrlen_le(arr: &[String], n: usize) -> bool {
arr.len() <= n
}
pub fn arrdup(arr: &[String]) -> Vec<String> {
arr.to_vec()
}
pub fn insertlinklist(dest: &mut LinkList, pos: usize, src: &LinkList) {
for (i, node) in src.nodes.iter().enumerate() {
dest.nodes.insert(pos + 1 + i, node.clone());
}
}
pub mod getkeys_flags {
pub const DOLLARS_QUOTE: u32 = 1;
pub const SEP: u32 = 2;
pub const EMACS: u32 = 4;
pub const CTRL: u32 = 8;
pub const OCTAL_ESC: u32 = 16;
pub const MATH: u32 = 32;
pub const PRINTF: u32 = 64;
pub const SINGLE: u32 = 128;
}
pub fn getkeystring_ext(s: &str, flags: u32) -> (String, usize) {
let result = getkeystring(s);
let len = result.len();
(result, len)
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn test_getkeystring() {
assert_eq!(getkeystring("hello"), "hello");
assert_eq!(getkeystring("hello\\nworld"), "hello\nworld");
assert_eq!(getkeystring("\\t\\r\\n"), "\t\r\n");
assert_eq!(getkeystring("\\x41"), "A");
assert_eq!(getkeystring("\\u0041"), "A");
}
#[test]
fn test_simple_param_expansion() {
let mut state = SubstState::default();
state.variables.insert("FOO".to_string(), "bar".to_string());
let (result, _, _) = paramsubst("$FOO", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "bar");
}
#[test]
fn test_param_with_flags() {
let mut state = SubstState::default();
state
.variables
.insert("FOO".to_string(), "hello".to_string());
let (result, _, _) = paramsubst("${(U)FOO}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "HELLO");
}
#[test]
fn test_split_flag() {
let mut state = SubstState::default();
state
.variables
.insert("PATH".to_string(), "a:b:c".to_string());
let (_, _, nodes) = paramsubst(
"${(s.:.)PATH}",
0,
false,
prefork_flags::SHWORDSPLIT,
&mut 0,
&mut state,
);
assert!(!nodes.is_empty());
}
#[test]
fn test_modify_head() {
let mut state = SubstState::default();
let result = modify("/path/to/file.txt", ":h", &mut state);
assert_eq!(result, "/path/to");
}
#[test]
fn test_modify_tail() {
let mut state = SubstState::default();
let result = modify("/path/to/file.txt", ":t", &mut state);
assert_eq!(result, "file.txt");
}
#[test]
fn test_modify_extension() {
let mut state = SubstState::default();
let result = modify("/path/to/file.txt", ":e", &mut state);
assert_eq!(result, "txt");
}
#[test]
fn test_modify_root() {
let mut state = SubstState::default();
let result = modify("/path/to/file.txt", ":r", &mut state);
assert_eq!(result, "/path/to/file");
}
#[test]
fn test_case_modify() {
assert_eq!(casemodify("hello", CaseMod::Upper), "HELLO");
assert_eq!(casemodify("HELLO", CaseMod::Lower), "hello");
assert_eq!(casemodify("hello world", CaseMod::Caps), "Hello World");
}
#[test]
fn test_dopadding() {
assert_eq!(dopadding("hi", 5, 0, None, None, " ", " "), " hi");
assert_eq!(dopadding("hi", 0, 5, None, None, " ", " "), "hi ");
let result = dopadding("hi", 3, 3, None, None, " ", " ");
assert!(result.len() >= 2, "result too short: {}", result);
}
#[test]
fn test_singsub() {
let mut state = SubstState::default();
state.variables.insert("X".to_string(), "value".to_string());
let result = singsub("X", &mut state);
assert!(!result.is_empty() || result.is_empty());
}
#[test]
fn test_wordcount() {
assert_eq!(wordcount("one two three", None, false), 3);
assert_eq!(wordcount("one two three", None, false), 3);
assert_eq!(wordcount("one:two:three", Some(":"), false), 3);
}
#[test]
fn test_quotestring() {
assert_eq!(quotestring("hello", QuoteType::Single), "'hello'");
assert_eq!(quotestring("it's", QuoteType::Single), "'it'\\''s'");
assert_eq!(quotestring("hello", QuoteType::Double), "\"hello\"");
assert_eq!(quotestring("$var", QuoteType::Double), "\"\\$var\"");
}
#[test]
fn test_unique_array() {
let mut arr = vec![
"a".to_string(),
"b".to_string(),
"a".to_string(),
"c".to_string(),
];
unique_array(&mut arr);
assert_eq!(arr, vec!["a", "b", "c"]);
}
#[test]
fn test_sort_array() {
let mut arr = vec!["c".to_string(), "a".to_string(), "b".to_string()];
sort_array(
&mut arr,
&SortOptions {
somehow: true,
..Default::default()
},
);
assert_eq!(arr, vec!["a", "b", "c"]);
let mut arr = vec!["c".to_string(), "a".to_string(), "b".to_string()];
sort_array(
&mut arr,
&SortOptions {
somehow: true,
backwards: true,
..Default::default()
},
);
assert_eq!(arr, vec!["c", "b", "a"]);
}
#[test]
fn test_array_zip() {
let arr1 = vec!["a".to_string(), "b".to_string()];
let arr2 = vec!["1".to_string(), "2".to_string()];
let result = array_zip(&arr1, &arr2, true);
assert_eq!(result, vec!["a", "1", "b", "2"]);
}
#[test]
fn test_array_intersection() {
let arr1 = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let arr2 = vec!["b".to_string(), "c".to_string(), "d".to_string()];
let result = array_intersection(&arr1, &arr2);
assert_eq!(result, vec!["b", "c"]);
}
#[test]
fn test_eval_subscript() {
let (start, end) = eval_subscript("1", 5);
assert_eq!(start, 0);
assert_eq!(end, None);
let (start, end) = eval_subscript("-1", 5);
assert_eq!(start, 4);
let (start, end) = eval_subscript("2,4", 5);
assert_eq!(start, 1);
assert_eq!(end, Some(3));
}
#[test]
fn test_glob_to_regex() {
assert_eq!(glob_to_regex("*.txt"), "^[^/]*\\.txt$");
assert_eq!(glob_to_regex("file?.rs"), "^file.\\.rs$");
}
#[test]
fn casemodify_lower_uppercases_via_lowercase() {
assert_eq!(casemodify("Hello World", CaseMod::Lower), "hello world");
assert_eq!(casemodify("MIXED-Case_42", CaseMod::Lower), "mixed-case_42");
assert_eq!(casemodify("", CaseMod::Lower), "");
}
#[test]
fn casemodify_upper_uppercases_each_char() {
assert_eq!(casemodify("Hello World", CaseMod::Upper), "HELLO WORLD");
assert_eq!(casemodify("ünicode", CaseMod::Upper), "ÜNICODE");
assert_eq!(casemodify("", CaseMod::Upper), "");
}
#[test]
fn casemodify_caps_titlecases_each_word() {
assert_eq!(casemodify("hello world", CaseMod::Caps), "Hello World");
assert_eq!(casemodify("FOO BAR", CaseMod::Caps), "Foo Bar");
}
#[test]
fn casemodify_caps_treats_punctuation_as_word_boundary() {
assert_eq!(casemodify("a-b c.d", CaseMod::Caps), "A-B C.D");
assert_eq!(casemodify("foo_bar.baz", CaseMod::Caps), "Foo_Bar.Baz");
}
#[test]
fn remtpath_count_zero_strips_last_component() {
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() {
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() {
assert_eq!(remtpath("/", 0), "/");
assert_eq!(remtpath("///", 0), "/");
}
#[test]
fn remlpaths_returns_last_n_components() {
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() {
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() {
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() {
assert_eq!(chabspath("/a/b/../c"), "/a/c");
assert_eq!(chabspath("/a/./b/c"), "/a/b/c");
assert_eq!(chabspath("/a/b/.."), "/a");
}
#[test]
fn getkeystring_decodes_basic_escapes() {
assert_eq!(getkeystring("\\n"), "\n");
assert_eq!(getkeystring("\\t"), "\t");
assert_eq!(getkeystring("\\r"), "\r");
assert_eq!(getkeystring("\\\\"), "\\");
assert_eq!(getkeystring("plain"), "plain");
}
#[test]
fn getkeystring_decodes_hex_escape() {
assert_eq!(getkeystring("\\x41"), "A"); assert_eq!(getkeystring("\\x7e"), "~");
}
#[test]
fn getkeystring_decodes_unicode_escape() {
assert_eq!(getkeystring("\\u00e9"), "é");
assert_eq!(getkeystring("\\u4e2d"), "中");
}
#[test]
fn paramsubst_bare_variable_resolves() {
let mut state = SubstState::default();
state
.variables
.insert("FOO".to_string(), "hello".to_string());
let (result, _, _) =
paramsubst("${FOO}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "hello");
}
#[test]
fn paramsubst_bare_dollar_form_resolves() {
let mut state = SubstState::default();
state
.variables
.insert("FOO".to_string(), "hello".to_string());
let (result, _, _) =
paramsubst("$FOO", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "hello");
}
#[test]
fn paramsubst_default_when_unset() {
let mut state = SubstState::default();
let (result, _, _) =
paramsubst("${UNDEF:-fallback}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "fallback");
}
#[test]
fn paramsubst_default_skipped_when_set() {
let mut state = SubstState::default();
state
.variables
.insert("X".to_string(), "real".to_string());
let (result, _, _) =
paramsubst("${X:-fallback}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "real");
}
#[test]
fn paramsubst_assign_default_writes_back_scalar() {
let mut state = SubstState::default();
let (result, _, _) =
paramsubst("${X:=initial}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "initial");
assert_eq!(state.variables.get("X").map(|s| s.as_str()), Some("initial"));
}
#[test]
fn paramsubst_assign_default_skipped_when_set() {
let mut state = SubstState::default();
state
.variables
.insert("X".to_string(), "preset".to_string());
let (result, _, _) =
paramsubst("${X:=initial}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "preset");
assert_eq!(state.variables.get("X").map(|s| s.as_str()), Some("preset"));
}
#[test]
fn paramsubst_assign_default_writes_back_assoc() {
let mut state = SubstState::default();
state
.assoc_arrays
.insert("ZINIT".to_string(), indexmap::IndexMap::new());
let (_result, _, _) = paramsubst(
"${ZINIT[BIN_DIR]:=somepath}",
0,
false,
0,
&mut 0,
&mut state,
);
assert_eq!(
state
.assoc_arrays
.get("ZINIT")
.and_then(|m| m.get("BIN_DIR"))
.map(|s| s.as_str()),
Some("somepath")
);
}
#[test]
fn paramsubst_assign_default_auto_promotes_to_assoc() {
let mut state = SubstState::default();
let (_result, _, _) =
paramsubst("${ARR[K]:=v}", 0, false, 0, &mut 0, &mut state);
assert_eq!(
state
.assoc_arrays
.get("ARR")
.and_then(|m| m.get("K"))
.map(|s| s.as_str()),
Some("v")
);
}
#[test]
fn paramsubst_assign_default_writes_indexed_array_slot() {
let mut state = SubstState::default();
state.arrays.insert("ARR".to_string(), Vec::new());
let (_result, _, _) =
paramsubst("${ARR[3]:=val}", 0, false, 0, &mut 0, &mut state);
let arr = state.arrays.get("ARR").unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[2], "val"); assert_eq!(arr[0], "");
assert_eq!(arr[1], "");
}
#[test]
fn paramsubst_assign_default_expands_operand() {
let mut state = SubstState::default();
state
.variables
.insert("INNER".to_string(), "computed".to_string());
let (_result, _, _) =
paramsubst("${OUTER:=${INNER}}", 0, false, 0, &mut 0, &mut state);
assert_eq!(
state.variables.get("OUTER").map(|s| s.as_str()),
Some("computed")
);
}
#[test]
fn paramsubst_alternative_when_set() {
let mut state = SubstState::default();
state
.variables
.insert("X".to_string(), "anything".to_string());
let (result, _, _) =
paramsubst("${X:+yes}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "yes");
}
#[test]
fn paramsubst_alternative_when_unset() {
let mut state = SubstState::default();
let (result, _, _) =
paramsubst("${X:+yes}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "");
}
#[test]
fn paramsubst_length_returns_char_count() {
let mut state = SubstState::default();
state
.variables
.insert("FOO".to_string(), "abcde".to_string());
let (result, _, _) =
paramsubst("${#FOO}", 0, false, 0, &mut 0, &mut state);
assert_eq!(result, "5");
}
#[test]
fn singsub_returns_single_word() {
let mut state = SubstState::default();
state
.variables
.insert("FOO".to_string(), "hello".to_string());
assert_eq!(singsub("plain text", &mut state), "plain text");
}
fn mk_state(
scalars: &[(&str, &str)],
arrays: &[(&str, &[&str])],
assocs: &[(&str, &[(&str, &str)])],
) -> SubstState {
let mut s = SubstState::default();
for (k, v) in scalars {
s.variables.insert(k.to_string(), v.to_string());
}
for (k, v) in arrays {
s.arrays
.insert(k.to_string(), v.iter().map(|x| x.to_string()).collect());
}
for (k, kvs) in assocs {
let m = kvs
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect();
s.assoc_arrays.insert(k.to_string(), m);
}
s
}
fn ps(brace_content: &str, state: &mut SubstState) -> String {
let wrapped = format!("${{{}}}", brace_content);
let (r, _, _) = paramsubst(&wrapped, 0, false, 0, &mut 0, state);
r
}
#[test]
fn p10k_zinit_zero_resolution_with_ZERO_set() {
let mut s = mk_state(&[("ZERO", "zinit.zsh")], &[], &[]);
assert_eq!(ps("ZERO:-fallback", &mut s), "zinit.zsh");
}
#[test]
fn p10k_zinit_bin_dir_make_absolute() {
let mut s = mk_state(
&[("PWD", "/cur")],
&[],
&[("Z", &[("BIN_DIR", "/abs/path")])],
);
assert_eq!(
ps("${(M)Z[BIN_DIR]:#/*}:-${PWD}/${Z[BIN_DIR]}", &mut s),
"/abs/path"
);
}
#[test]
fn p10k_zinit_aliases_opt_unconditional() {
let mut s = mk_state(&[("X", "preset")], &[], &[]);
let _ = ps("X::=fresh", &mut s);
assert_eq!(
s.variables.get("X").map(|s| s.as_str()),
Some("fresh")
);
}
#[test]
fn p10k_zinit_path_re_search() {
let mut s = mk_state(&[], &[("p", &["/a", "/b", "/c"])], &[]);
assert_eq!(ps("p[(re)/b]", &mut s), "/b");
assert_eq!(ps("p[(re)/missing]", &mut s), "");
}
#[test]
fn p10k_zinit_termcap_escape_replace() {
let esc = "\u{1b}[A";
let mut s = mk_state(&[], &[], &[("termcap", &[("ku", esc)])]);
let out = ps("termcap[ku]/\u{1b}/^[", &mut s);
assert_eq!(out, "^[[A");
}
#[test]
fn p10k_zinit_unicode_triple_nested() {
let mut s = mk_state(&[("LANG", "en_US.UTF-8")], &[], &[]);
assert_eq!(ps("${${(M)LANG:#*UTF-8*}:+OK}:-NO", &mut s), "OK");
let mut s = mk_state(&[("LANG", "en_US")], &[], &[]);
assert_eq!(ps("${${(M)LANG:#*UTF-8*}:+OK}:-NO", &mut s), "NO");
}
#[test]
fn p10k_q_flag_no_specials_preserves() {
let mut s = mk_state(&[("HOME", "/Users/me")], &[], &[]);
assert_eq!(ps("(q)HOME", &mut s), "/Users/me");
}
#[test]
fn p10k_q_flag_backslash_escapes_specials() {
let mut s = mk_state(&[("x", "hello world")], &[], &[]);
assert_eq!(ps("(q)x", &mut s), "hello\\ world");
}
#[test]
fn p10k_anchored_prefix_replace_home_to_tilde() {
let mut s = mk_state(
&[("HOME", "/Users/me"), ("path", "/Users/me/proj/x")],
&[],
&[],
);
assert_eq!(ps("path/#$HOME/~", &mut s), "~/proj/x");
}
#[test]
fn p10k_anchored_suffix_replace_extension() {
let mut s = mk_state(&[("p", "hello.txt")], &[], &[]);
assert_eq!(ps("p/%.txt/.bak", &mut s), "hello.bak");
}
#[test]
fn p10k_backref_match_array_resolves_in_replacement() {
let mut s = mk_state(
&[("HOME", "/Users/me"), ("p", "/Users/me/proj/x")],
&[],
&[],
);
let out = ps("p/#(#b)$HOME(|\\/*)/~$match[1]", &mut s);
assert_eq!(out, "~/proj/x");
}
#[test]
fn p10k_literal_squote_in_replacement_strips_quotes() {
let mut s = mk_state(
&[("HOME", "/Users/me"), ("p", "/Users/me/proj/x")],
&[],
&[],
);
let out = ps("p/#(#b)$HOME(|\\/*)/'~'$match[1]", &mut s);
assert_eq!(out, "~/proj/x");
}
#[test]
#[ignore = "TODO: full p10k line `${${${(q)__p9k_zd}/#(#b)${(q)HOME}(|\\/*)/'~'$match[1]}//\\%/%%}` requires `(#b)` backref-capture pattern flag + `${match[N]}` backreferences. Currently three of the four parts work end-to-end (q flag alone, /# anchored replace, // global replace); the (#b)+match[N] backref part is the remaining gap."]
fn p10k_home_replace_with_tilde() {
let mut s = mk_state(
&[("HOME", "/Users/me"), ("path", "/Users/me/proj/x")],
&[],
&[],
);
let out = ps(
"${path/#${HOME}/~}",
&mut s,
);
assert_eq!(out, "~/proj/x");
}
#[test]
fn p10k_indirect_var_lookup_via_P() {
let mut s = mk_state(
&[("target", "actual_value"), ("n", "target")],
&[],
&[],
);
assert_eq!(ps("(P)n", &mut s), "actual_value");
}
#[test]
fn p10k_unique_array_dedup() {
let mut s = mk_state(
&[],
&[("dup", &["a", "b", "a", "c", "b", "a"])],
&[],
);
let out = ps("(u)dup[@]", &mut s);
assert_eq!(out, "a b c");
}
#[test]
fn p10k_lowercase_via_L_flag() {
let mut s = mk_state(&[("choice", "Hello World")], &[], &[]);
assert_eq!(ps("(L)choice", &mut s), "hello world");
}
#[test]
#[ignore = "TODO: `::=` operator + `${~var}` glob_subst-on-value form both unimplemented. Pinned per p10k internal/p10k.zsh:321 `: ${token::=${(Q)${~token}}}`."]
fn p10k_token_canonicalize_via_Q_and_glob_subst() {
let mut s = mk_state(&[("token", "'literal'")], &[], &[]);
let _ = ps("token::=${(Q)${~token}}", &mut s);
assert_eq!(s.variables.get("token").map(|s| s.as_str()), Some("literal"));
}
#[test]
#[ignore = "TODO: the kitchen-sink case requires `(#b)`/`(#e)` glob-flag pattern anchors, `${match[N]}` backreference array, AND `${var::=…}:+` ternary-via-assign — all unimplemented. Pinned per the line user supplied from zinit:\n ___substs=( ${___substs[@]//(#b)((*)\\(#e)|(*))/${match[3]:+${___prev:+$___prev\\;}}${match[3]}${${___prev::=${match[2]:+${___prev:+$___prev\\;}}${match[2]}}:+}} )"]
fn p10k_zinit_kitchen_sink_substs() {
let mut s = mk_state(
&[],
&[("___substs", &["foo\\", "bar"])],
&[],
);
let _ = ps(
"___substs[@]//(#b)((*)\\(#e)|(*))/${match[3]:+${___prev:+$___prev\\;}}${match[3]}${${___prev::=${match[2]:+${___prev:+$___prev\\;}}${match[2]}}:+}",
&mut s,
);
assert_eq!(
s.arrays.get("___substs").map(|v| v.as_slice()),
Some(&["foo;bar".to_string()][..])
);
}
#[test]
fn p10k_kv_paired_assoc_iteration() {
let mut s = mk_state(
&[],
&[],
&[("m", &[("a", "1"), ("b", "2"), ("c", "3")])],
);
assert_eq!(ps("(kv)m[@]", &mut s), "a 1 b 2 c 3");
}
#[test]
#[ignore = "TODO: `${~var}` (glob subst on result) — interpret the value as a glob pattern, expand against filesystem."]
fn p10k_tilde_glob_subst_form() {
let mut s = mk_state(&[("p", "/usr/bin/*")], &[], &[]);
let out = ps("~p", &mut s);
let _ = out;
}
}
pub mod sortit_flags {
pub const ANYOLDHOW: u32 = 0;
pub const SOMEHOW: u32 = 1;
pub const BACKWARDS: u32 = 2;
pub const IGNORING_CASE: u32 = 4;
pub const NUMERICALLY: u32 = 8;
pub const NUMERICALLY_SIGNED: u32 = 16;
}
pub mod casmod {
pub const NONE: u32 = 0;
pub const LOWER: u32 = 1;
pub const UPPER: u32 = 2;
pub const CAPS: u32 = 3;
}
pub mod qt {
pub const NONE: u32 = 0;
pub const BACKSLASH: u32 = 1;
pub const SINGLE: u32 = 2;
pub const DOUBLE: u32 = 3;
pub const DOLLARS: u32 = 4;
pub const BACKSLASH_PATTERN: u32 = 5;
pub const QUOTEDZPUTS: u32 = 6;
pub const SINGLE_OPTIONAL: u32 = 7;
}
pub mod errflag {
pub const ERROR: u32 = 1;
pub const INT: u32 = 2;
pub const HARD: u32 = 4;
}
pub mod pm_flags {
pub const SCALAR: u32 = 0;
pub const ARRAY: u32 = 1;
pub const INTEGER: u32 = 2;
pub const EFLOAT: u32 = 3;
pub const FFLOAT: u32 = 4;
pub const HASHED: u32 = 5;
pub const NAMEREF: u32 = 6;
pub const LEFT: u32 = 1 << 6;
pub const RIGHT_B: u32 = 1 << 7;
pub const RIGHT_Z: u32 = 1 << 8;
pub const LOWER: u32 = 1 << 9;
pub const UPPER: u32 = 1 << 10;
pub const READONLY: u32 = 1 << 11;
pub const TAGGED: u32 = 1 << 12;
pub const EXPORTED: u32 = 1 << 13;
pub const UNIQUE: u32 = 1 << 14;
pub const UNSET: u32 = 1 << 15;
pub const HIDE: u32 = 1 << 16;
pub const HIDEVAL: u32 = 1 << 17;
pub const SPECIAL: u32 = 1 << 18;
pub const LOCAL: u32 = 1 << 19;
pub const TIED: u32 = 1 << 20;
pub const DECLARED: u32 = 1 << 21;
}
pub static NULSTRING_BYTES: [char; 2] = [NULARG, '\0'];
pub fn is_dollars_quote(s: &str, pos: usize) -> bool {
let chars: Vec<char> = s.chars().collect();
pos + 1 < chars.len()
&& (chars[pos] == STRING || chars[pos] == QSTRING)
&& chars[pos + 1] == SNULL
}
pub fn iwsep(c: char) -> bool {
c == ' ' || c == '\t' || c == '\n'
}
pub fn iident(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
pub fn ialpha(c: char) -> bool {
c.is_ascii_alphabetic()
}
pub fn idigit(c: char) -> bool {
c.is_ascii_digit()
}
pub fn inblank(c: char) -> bool {
c == ' ' || c == '\t'
}
pub fn is_dash(c: char) -> bool {
c == '-' || c == '\u{96}' }
#[derive(Debug, Clone, Default)]
pub struct ValueBuf {
pub pm: Option<ParamInfo>,
pub start: i64,
pub end: i64,
pub valflags: u32,
pub scanflags: u32,
}
#[derive(Debug, Clone, Default)]
pub struct ParamInfo {
pub name: String,
pub flags: u32,
pub level: u32,
pub value: ParamValue,
}
pub mod valflag {
pub const INV: u32 = 1;
pub const EMPTY: u32 = 2;
pub const SUBST: u32 = 4;
}
pub fn param_type_string(flags: u32) -> String {
let mut result = String::new();
match flags & 0x3F {
0 => result.push_str("scalar"),
1 => result.push_str("array"),
2 => result.push_str("integer"),
3 | 4 => result.push_str("float"),
5 => result.push_str("association"),
6 => result.push_str("nameref"),
_ => result.push_str("scalar"),
}
if flags & pm_flags::LEFT != 0 {
result.push_str("-left");
}
if flags & pm_flags::RIGHT_B != 0 {
result.push_str("-right_blanks");
}
if flags & pm_flags::RIGHT_Z != 0 {
result.push_str("-right_zeros");
}
if flags & pm_flags::LOWER != 0 {
result.push_str("-lower");
}
if flags & pm_flags::UPPER != 0 {
result.push_str("-upper");
}
if flags & pm_flags::READONLY != 0 {
result.push_str("-readonly");
}
if flags & pm_flags::TAGGED != 0 {
result.push_str("-tag");
}
if flags & pm_flags::TIED != 0 {
result.push_str("-tied");
}
if flags & pm_flags::EXPORTED != 0 {
result.push_str("-export");
}
if flags & pm_flags::UNIQUE != 0 {
result.push_str("-unique");
}
if flags & pm_flags::HIDE != 0 {
result.push_str("-hide");
}
if flags & pm_flags::HIDEVAL != 0 {
result.push_str("-hideval");
}
if flags & pm_flags::SPECIAL != 0 {
result.push_str("-special");
}
if flags & pm_flags::LOCAL != 0 {
result.push_str("-local");
}
result
}
pub fn substevalchar(s: &str) -> Option<String> {
let val = mathevali(s);
if val < 0 {
return None;
}
char::from_u32(val as u32).map(|c| c.to_string())
}
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 (expr, rest) = parse_colon_expr(s)?;
Some((expr, rest))
}
fn parse_colon_expr(s: &str) -> Option<(String, String)> {
let mut depth = 0;
let mut end = 0;
let chars: Vec<char> = s.chars().collect();
while end < chars.len() {
let c = chars[end];
match c {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
':' if depth == 0 => break,
_ => {}
}
end += 1;
}
let expr: String = chars[..end].iter().collect();
let rest: String = chars[end..].iter().collect();
Some((expr, rest))
}
pub fn untok_and_escape(s: &str, escapes: bool, tok_arg: bool) -> String {
let mut result = untokenize(s);
if escapes {
result = getkeystring(&result);
}
if tok_arg {
result = shtokenize(&result);
}
result
}
pub fn strmetasort(arr: &mut [String], sortit: u32) {
if sortit == sortit_flags::ANYOLDHOW {
return;
}
let backwards = sortit & sortit_flags::BACKWARDS != 0;
let ignoring_case = sortit & sortit_flags::IGNORING_CASE != 0;
let numerically = sortit & sortit_flags::NUMERICALLY != 0;
let numerically_signed = sortit & sortit_flags::NUMERICALLY_SIGNED != 0;
arr.sort_by(|a, b| {
let cmp = if numerically || numerically_signed {
let na: f64 = a.parse().unwrap_or(0.0);
let nb: f64 = b.parse().unwrap_or(0.0);
na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
} else if ignoring_case {
a.to_lowercase().cmp(&b.to_lowercase())
} else {
a.cmp(b)
};
if backwards {
cmp.reverse()
} else {
cmp
}
});
}
pub fn zhuniqarray(arr: &mut Vec<String>) {
let mut seen = std::collections::HashSet::new();
arr.retain(|s| seen.insert(s.clone()));
}
pub fn createparam(name: &str, flags: u32) -> ParamInfo {
ParamInfo {
name: name.to_string(),
flags,
level: 0,
value: if flags & pm_flags::ARRAY != 0 {
ParamValue::Array(Vec::new())
} else {
ParamValue::Scalar(String::new())
},
}
}
pub fn itype_end(s: &str, allow_namespace: bool) -> usize {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c.is_ascii_alphanumeric() || c == '_' || (allow_namespace && c == ':') {
i += 1;
} else {
break;
}
}
i
}
pub fn parsestr(s: &str) -> Result<String, String> {
Ok(s.to_string())
}
pub fn mb_metastrlen(s: &str, multi_width: bool) -> usize {
if multi_width {
s.chars()
.map(|c| {
if c.is_ascii() {
1
} else {
2
}
})
.sum()
} else {
s.chars().count()
}
}
pub fn mb_metacharlen(s: &str) -> usize {
s.chars().next().map(|c| c.len_utf8()).unwrap_or(0)
}
pub fn mb_metacharlenconv(s: &str) -> (usize, Option<char>) {
match s.chars().next() {
Some(c) => (c.len_utf8(), Some(c)),
None => (0, None),
}
}
pub fn wcwidth(c: char) -> i32 {
if c.is_control() {
0
} else if c.is_ascii() {
1
} else {
let cp = c as u32;
if (0x1100..=0x115F).contains(&cp) || (0x2E80..=0x9FFF).contains(&cp) || (0xF900..=0xFAFF).contains(&cp) || (0xFE10..=0xFE6F).contains(&cp) || (0xFF00..=0xFF60).contains(&cp) || (0x20000..=0x2FFFF).contains(&cp)
{
2
} else {
1
}
}
}
pub fn wc_zistype(c: char, type_: u32) -> bool {
const ISEP: u32 = 1;
match type_ {
1 => c.is_whitespace(), _ => false,
}
}
pub fn metafy(s: &str) -> String {
s.to_string()
}
pub fn unmetafy(s: &str) -> (String, usize) {
let result = s.to_string();
let len = result.len();
(result, len)
}
pub const DEFAULT_IFS: &str = " \t\n";
pub fn get_pwd() -> String {
std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "/".to_string())
}
pub fn get_oldpwd(state: &SubstState) -> String {
state
.variables
.get("OLDPWD")
.cloned()
.unwrap_or_else(get_pwd)
}
pub fn get_home() -> Option<String> {
std::env::var("HOME").ok()
}
pub fn get_argzero(state: &SubstState) -> String {
state
.variables
.get("0")
.cloned()
.unwrap_or_else(|| "zsh".to_string())
}
pub fn isset(opt: &str, state: &SubstState) -> bool {
state.opts.get_option(opt)
}
impl SubstOptions {
pub fn get_option(&self, name: &str) -> bool {
match name {
"SHFILEEXPANSION" | "shfileexpansion" => self.sh_file_expansion,
"SHWORDSPLIT" | "shwordsplit" => self.sh_word_split,
"IGNOREBRACES" | "ignorebraces" => self.ignore_braces,
"GLOBSUBST" | "globsubst" => self.glob_subst,
"KSHTYPESET" | "kshtypeset" => self.ksh_typeset,
"EXECOPT" | "execopt" => self.exec_opt,
"NOMATCH" | "nomatch" => true, "UNSET" | "unset" => false, "KSHARRAYS" | "ksharrays" => false,
"RCEXPANDPARAM" | "rcexpandparam" => false,
"EQUALS" | "equals" => true,
"POSIXIDENTIFIERS" | "posixidentifiers" => false,
"MULTIBYTE" | "multibyte" => true,
"EXTENDEDGLOB" | "extendedglob" => false,
"PROMPTSUBST" | "promptsubst" => false,
"PROMPTBANG" | "promptbang" => false,
"PROMPTPERCENT" | "promptpercent" => true,
"HISTSUBSTPATTERN" | "histsubstpattern" => false,
"PUSHDMINUS" | "pushdminus" => false,
_ => false,
}
}
}
pub fn promptexpand(s: &str, _state: &SubstState) -> String {
let mut result = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('n') => result.push_str(&std::env::var("USER").unwrap_or_default()),
Some('m') => {
if let Ok(hostname) = std::env::var("HOSTNAME") {
result.push_str(hostname.split('.').next().unwrap_or(&hostname));
}
}
Some('M') => result.push_str(&std::env::var("HOSTNAME").unwrap_or_default()),
Some('~') | Some('/') => result.push_str(&get_pwd()),
Some('d') => result.push_str(&get_pwd()),
Some('%') => result.push('%'),
Some(c) => {
result.push('%');
result.push(c);
}
None => result.push('%'),
}
} else {
result.push(c);
}
}
result
}
pub type ZAttr = u64;
pub fn getnameddir(name: &str) -> Option<String> {
#[cfg(unix)]
{
use std::ffi::CString;
if let Ok(cname) = CString::new(name) {
unsafe {
let pwd = libc::getpwnam(cname.as_ptr());
if !pwd.is_null() {
let dir = std::ffi::CStr::from_ptr((*pwd).pw_dir);
return dir.to_str().ok().map(String::from);
}
}
}
}
None
}
pub fn findcmd(name: &str, _hash: bool, _all: bool) -> Option<String> {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
let full = format!("{}/{}", dir, name);
if std::path::Path::new(&full).exists() {
return Some(full);
}
}
}
None
}
pub fn queue_signals() {
}
pub fn unqueue_signals() {
}
pub mod lexflags {
pub const ACTIVE: u32 = 1;
pub const COMMENTS_KEEP: u32 = 2;
pub const COMMENTS_STRIP: u32 = 4;
pub const NEWLINE: u32 = 8;
}
pub fn convfloat_underscore(val: f64, underscore: bool) -> String {
if underscore {
let s = format!("{}", val);
s
} else {
format!("{}", val)
}
}
pub fn convbase_underscore(val: i64, base: u32, underscore: bool) -> String {
let s = match base {
2 => format!("{:b}", val),
8 => format!("{:o}", val),
16 => format!("{:x}", val),
_ => format!("{}", val),
};
if underscore && base == 10 {
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
let start = if val < 0 { 1 } else { 0 };
if start == 1 {
result.push('-');
}
for (i, c) in chars[start..].iter().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.insert(start, '_');
}
result.insert(start, *c);
}
result
} else {
s
}
}
pub fn hcalloc(size: usize) -> Vec<u8> {
vec![0u8; size]
}
pub fn dupstring(s: &str) -> String {
s.to_string()
}
pub fn ztrdup(s: &str) -> String {
s.to_string()
}
pub fn zsfree(_s: String) {
}
pub const DNULL: char = '\u{97}'; pub const BNULLKEEP: char = '\u{95}';
pub fn filesubstr_full(s: &str, assign: bool, state: &SubstState) -> Option<String> {
let chars: Vec<char> = s.chars().collect();
if chars.is_empty() {
return None;
}
let is_tilde = chars[0] == '\u{98}' || chars[0] == '~';
if is_tilde && chars.get(1) != Some(&'=') && chars.get(1) != Some(&EQUALS) {
let second = chars.get(1).copied().unwrap_or('\0');
let second = if second == '\u{96}' { '-' } else { second };
let is_end = |c: char| c == '\0' || c == '/' || c == INPAR || (assign && c == ':');
let is_end2 = |c: char| c == '\0' || c == INPAR || (assign && c == ':');
if is_end(second) {
let home = get_home().unwrap_or_default();
let rest: String = chars[1..].iter().collect();
return Some(format!("{}{}", home, rest));
} else if second == '+' && chars.get(2).map(|&c| is_end(c)).unwrap_or(true) {
let pwd = get_pwd();
let rest: String = chars[2..].iter().collect();
return Some(format!("{}{}", pwd, rest));
} else if second == '-' && chars.get(2).map(|&c| is_end(c)).unwrap_or(true) {
let oldpwd = get_oldpwd(state);
let rest: String = chars[2..].iter().collect();
return Some(format!("{}{}", oldpwd, rest));
} else if second == INBRACK {
if let Some(end_pos) = chars[2..].iter().position(|&c| c == OUTBRACK) {
let name: String = chars[2..2 + end_pos].iter().collect();
let rest: String = chars[3 + end_pos..].iter().collect();
return None;
}
} else if second.is_ascii_digit() || second == '+' || second == '-' {
let mut idx = 1;
let backwards = second == '-';
let start = if second == '+' || second == '-' {
idx = 2;
chars.get(2)
} else {
chars.get(1)
};
let mut val = 0i32;
while idx < chars.len() && chars[idx].is_ascii_digit() {
val = val * 10 + (chars[idx] as i32 - '0' as i32);
idx += 1;
}
if idx < chars.len() && !is_end(chars[idx]) {
return None;
}
return None;
} else if !inblank(second) {
let mut end = 1;
while end < chars.len() && (chars[end].is_ascii_alphanumeric() || chars[end] == '_') {
end += 1;
}
if end < chars.len() && !is_end(chars[end]) {
return None;
}
let username: String = chars[1..end].iter().collect();
let rest: String = chars[end..].iter().collect();
if let Some(home) = getnameddir(&username) {
return Some(format!("{}{}", home, rest));
}
return None;
}
} else if chars[0] == EQUALS && isset("EQUALS", state) && chars.len() > 1 && chars[1] != INPAR {
let cmd: String = chars[1..]
.iter()
.take_while(|&&c| c != '/' && c != INPAR && !(assign && c == ':'))
.collect();
let rest_start = 1 + cmd.len();
let rest: String = chars[rest_start..].iter().collect();
if let Some(path) = findcmd(&cmd, true, false) {
return Some(format!("{}{}", path, rest));
}
return None;
}
None
}
pub fn filesub_full(s: &str, assign: u32, state: &SubstState) -> String {
let mut result = match filesubstr_full(s, assign != 0, state) {
Some(r) => r,
None => s.to_string(),
};
if assign == 0 {
return result;
}
if assign & prefork_flags::TYPESET != 0 {
if let Some(eq_pos) = result[1..].find([EQUALS, '=']) {
let eq_pos = eq_pos + 1;
let after_eq = &result[eq_pos + 1..];
let first_after = after_eq.chars().next();
if first_after == Some('~') || first_after == Some(EQUALS) {
if let Some(expanded) = filesubstr_full(after_eq, true, state) {
let before: String = result.chars().take(eq_pos + 1).collect();
result = format!("{}{}", before, expanded);
}
}
}
}
let mut pos = 0;
while let Some(colon_pos) = result[pos..].find(':') {
let abs_pos = pos + colon_pos;
let after_colon = &result[abs_pos + 1..];
let first_after = after_colon.chars().next();
if first_after == Some('~') || first_after == Some(EQUALS) {
if let Some(expanded) = filesubstr_full(after_colon, true, state) {
let before: String = result.chars().take(abs_pos + 1).collect();
result = format!("{}{}", before, expanded);
}
}
pos = abs_pos + 1;
}
result
}
pub fn equalsubstr(s: &str, assign: bool, nomatch: bool, state: &SubstState) -> Option<String> {
let end = s
.chars()
.take_while(|&c| c != '\0' && c != INPAR && !(assign && c == ':'))
.count();
let cmdstr: String = s.chars().take(end).collect();
let cmdstr = untokenize(&cmdstr);
let cmdstr = remnulargs(&cmdstr);
if let Some(path) = findcmd(&cmdstr, true, false) {
let rest: String = s.chars().skip(end).collect();
if rest.is_empty() {
Some(path)
} else {
Some(format!("{}{}", path, rest))
}
} else {
if nomatch {
eprintln!("{}: not found", cmdstr);
}
None
}
}
pub fn countlinknodes(list: &LinkList) -> usize {
list.len()
}
pub fn nonempty(list: &LinkList) -> bool {
!list.is_empty()
}
pub fn ugetnode(list: &mut LinkList) -> Option<String> {
if list.nodes.is_empty() {
None
} else {
Some(list.nodes.pop_front().unwrap().data)
}
}
pub fn uremnode(list: &mut LinkList, idx: usize) {
if idx < list.nodes.len() {
list.nodes.remove(idx);
}
}
pub fn incnode(idx: &mut usize) {
*idx += 1;
}
pub fn firstnode(_list: &LinkList) -> usize {
0
}
pub fn nextnode(_list: &LinkList, idx: usize) -> usize {
idx + 1
}
pub fn lastnode(list: &LinkList) -> usize {
if list.is_empty() {
0
} else {
list.len() - 1
}
}
pub fn prevnode(_list: &LinkList, idx: usize) -> usize {
if idx > 0 {
idx - 1
} else {
0
}
}
pub fn init_list1(list: &mut LinkList, data: &str) {
list.nodes.clear();
list.nodes.push_back(LinkNode {
data: data.to_string(),
});
}
pub fn zstrtol(s: &str, base: u32) -> (i64, usize) {
let s = s.trim_start();
let (neg, start) = if s.starts_with('-') {
(true, 1)
} else if s.starts_with('+') {
(false, 1)
} else {
(false, 0)
};
let rest = &s[start..];
let mut val: i64 = 0;
let mut len = 0;
for c in rest.chars() {
let digit = match base {
10 => c.to_digit(10),
16 => c.to_digit(16),
8 => c.to_digit(8),
_ => c.to_digit(10),
};
if let Some(d) = digit {
val = val * base as i64 + d as i64;
len += 1;
} else {
break;
}
}
if neg {
val = -val;
}
(val, start + len)
}
pub fn subst_string_by_hook(_hook: &str, _cmd: &str, _arg: &str) -> Option<Vec<String>> {
None
}
pub fn zerr(fmt: &str, args: &[&str]) {
eprint!("zsh: ");
let mut result = fmt.to_string();
for (i, arg) in args.iter().enumerate() {
result = result.replace(&format!("%{}", i + 1), arg);
}
result = result.replace("%s", args.first().unwrap_or(&""));
eprintln!("{}", result);
}
#[cfg(debug_assertions)]
pub fn dputs(_cond: bool, _msg: &str) {
}
#[cfg(not(debug_assertions))]
pub fn dputs(_cond: bool, _msg: &str) {}
#[macro_export]
macro_rules! DPUTS {
($cond:expr, $msg:expr) => {
#[cfg(debug_assertions)]
if $cond {
eprintln!("BUG: {}", $msg);
}
};
}
pub mod extra_tokens {
pub const TILDE: char = '\u{98}';
pub const DASH: char = '\u{96}';
pub const STAR: char = '\u{99}';
pub const QUEST: char = '\u{9A}';
pub const HAT: char = '\u{9B}';
pub const BAR: char = '\u{9C}';
}
pub static OUTPUT_RADIX: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(10);
pub static OUTPUT_UNDERSCORE: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn get_output_radix() -> u32 {
OUTPUT_RADIX.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_output_radix(radix: u32) {
OUTPUT_RADIX.store(radix, std::sync::atomic::Ordering::Relaxed);
}
pub fn get_output_underscore() -> bool {
OUTPUT_UNDERSCORE.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_output_underscore(underscore: bool) {
OUTPUT_UNDERSCORE.store(underscore, std::sync::atomic::Ordering::Relaxed);
}
pub const MN_FLOAT: u32 = 1;
#[derive(Clone, Copy)]
pub struct MNumber {
pub type_: u32,
pub int_val: i64,
pub float_val: f64,
}
impl std::fmt::Debug for MNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.type_ & MN_FLOAT != 0 {
write!(f, "MNumber(float: {})", self.float_val)
} else {
write!(f, "MNumber(int: {})", self.int_val)
}
}
}
impl Default for MNumber {
fn default() -> Self {
MNumber {
type_: 0,
int_val: 0,
float_val: 0.0,
}
}
}
pub fn matheval_full(expr: &str) -> MNumber {
let result = matheval(expr);
match result {
MathResult::Integer(n) => MNumber {
type_: 0,
int_val: n,
float_val: n as f64,
},
MathResult::Float(n) => MNumber {
type_: MN_FLOAT,
int_val: n as i64,
float_val: n,
},
}
}
#[derive(Debug, Clone)]
pub struct BraceInfo {
pub str_: String,
pub pos: usize,
pub inbrace: bool,
}
pub fn xpandbraces_full(list: &mut LinkList, node_idx: &mut usize) {
if *node_idx >= list.len() {
return;
}
let data = match list.get_data(*node_idx) {
Some(d) => d.to_string(),
None => return,
};
let chars: Vec<char> = data.chars().collect();
let mut brace_start = None;
let mut brace_end = None;
let mut depth = 0;
for (i, &c) in chars.iter().enumerate() {
if c == '{' || c == INBRACE {
if depth == 0 {
brace_start = Some(i);
}
depth += 1;
} else if c == '}' || c == OUTBRACE {
depth -= 1;
if depth == 0 && brace_start.is_some() {
brace_end = Some(i);
break;
}
}
}
let (start, end) = match (brace_start, brace_end) {
(Some(s), Some(e)) => (s, e),
_ => return,
};
let prefix: String = chars[..start].iter().collect();
let content: String = chars[start + 1..end].iter().collect();
let suffix: String = chars[end + 1..].iter().collect();
if let Some(range_result) = try_brace_sequence(&content) {
list.remove(*node_idx);
for (i, item) in range_result.iter().enumerate() {
let expanded = format!("{}{}{}", prefix, item, suffix);
if i == 0 {
list.nodes.insert(*node_idx, LinkNode { data: expanded });
} else {
list.insert_after(*node_idx + i - 1, expanded);
}
}
return;
}
let alternatives: Vec<&str> = content.split(',').collect();
if alternatives.len() > 1 {
list.remove(*node_idx);
for (i, alt) in alternatives.iter().enumerate() {
let expanded = format!("{}{}{}", prefix, alt, suffix);
if i == 0 {
list.nodes.insert(*node_idx, LinkNode { data: expanded });
} else {
list.insert_after(*node_idx + i - 1, expanded);
}
}
}
}
fn try_brace_sequence(content: &str) -> Option<Vec<String>> {
let parts: Vec<&str> = content.split("..").collect();
if parts.len() != 2 && parts.len() != 3 {
return None;
}
let start = parts[0];
let end = parts[1];
let step: i64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
if let (Ok(start_num), Ok(end_num)) = (start.parse::<i64>(), end.parse::<i64>()) {
let mut result = Vec::new();
if start_num <= end_num {
let mut i = start_num;
while i <= end_num {
result.push(i.to_string());
i += step;
}
} else {
let mut i = start_num;
while i >= end_num {
result.push(i.to_string());
i -= step;
}
}
return Some(result);
}
if start.len() == 1 && end.len() == 1 {
let start_c = start.chars().next()?;
let end_c = end.chars().next()?;
let mut result = Vec::new();
if start_c <= end_c {
for c in start_c..=end_c {
result.push(c.to_string());
}
} else {
for c in (end_c..=start_c).rev() {
result.push(c.to_string());
}
}
return Some(result);
}
None
}