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()
}
}
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, std::collections::HashMap<String, String>>,
}
#[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,
}
impl Default for SubstState {
fn default() -> Self {
SubstState {
errflag: false,
opts: SubstOptions::default(),
variables: std::collections::HashMap::new(),
arrays: std::collections::HashMap::new(),
assoc_arrays: std::collections::HashMap::new(),
}
}
}
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;
while node_idx < list.len() {
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 {
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)
}
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 c >= '0' && c <= '7' {
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;
while pos < str3.len() && !state.errflag {
let chars: Vec<char> = str3.chars().collect();
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;
while pos < str3.len() && !state.errflag {
let chars: Vec<char> = str3.chars().collect();
let c = chars[pos];
let qt = c == QSTRING;
if qt || c == STRING {
let next_c = chars.get(pos + 1).copied();
if next_c == Some(INPAR) || next_c == Some(INPARMATH) {
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_c == Some(INBRACK) {
let start = pos + 2;
if let Some(end) = find_matching_bracket(&str3[start..], INBRACK, OUTBRACK) {
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) {
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)
}
}
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 value = 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] == ':' {
pos += 1;
let mut sep = String::new();
while pos < chars.len() && chars[pos] != ':' {
sep.push(chars[pos]);
pos += 1;
}
flags.split_sep = Some(sep);
} else {
pos -= 1;
}
}
'j' => {
pos += 1;
if pos < chars.len() && chars[pos] == ':' {
pos += 1;
let mut sep = String::new();
while pos < chars.len() && chars[pos] != ':' {
sep.push(chars[pos]);
pos += 1;
}
flags.join_sep = Some(sep);
} 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 fill = String::new();
while pos < chars.len() && chars[pos] != ':' {
fill.push(chars[pos]);
pos += 1;
}
flags.pad_char = Some(fill.chars().next().unwrap_or(' '));
}
} 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 fill = String::new();
while pos < chars.len() && chars[pos] != ':' {
fill.push(chars[pos]);
pos += 1;
}
flags.pad_char = Some(fill.chars().next().unwrap_or(' '));
}
} else {
pos -= 1;
}
}
_ => {}
}
pos += 1;
}
if pos < chars.len() {
pos += 1; }
}
let length_prefix = chars.get(pos) == Some(&'#');
if length_prefix {
pos += 1;
}
let var_start = pos;
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();
let mut subscript = None;
if 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;
}
}
subscript = Some(chars[sub_start..pos].iter().collect::<String>());
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(":");
} }
}
}
'-' => {
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 {
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(",");
}
}
_ => {}
}
}
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 subscript.is_some() || !var_name.is_empty() {
get_param_with_subscript(&var_name, subscript.as_deref(), state)
} else {
Vec::new()
};
if length_prefix {
let len = if value.len() == 1 {
value[0].chars().count()
} else {
value.len()
};
value = vec![len.to_string()];
}
value = apply_param_flags(&value, &flags, state);
value = apply_operator(&var_name, value, operator, &operand, state);
let joined = if flags.join_sep.is_some() || value.len() == 1 {
let sep = flags.join_sep.as_deref().unwrap_or(" ");
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(" ")
};
let prefix: String = chars[..dollar_pos].iter().collect();
let suffix: String = chars[pos..].iter().collect();
let result = format!("{}{}{}", prefix, joined, suffix);
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>,
}
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> {
if let Some(arr) = state.arrays.get(name) {
if let Some(sub) = subscript {
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(sub) = subscript {
if sub == "@" || sub == "*" {
return assoc.values().cloned().collect();
}
return assoc.get(sub).cloned().into_iter().collect();
}
return assoc.values().cloned().collect();
}
let value = get_param_value(name, state);
if value.is_empty() {
Vec::new()
} else {
vec![value]
}
}
fn apply_param_flags(value: &[String], flags: &ParamFlags, _state: &SubstState) -> Vec<String> {
let mut result: Vec<String> = value.to_vec();
if let Some(ref sep) = flags.split_sep {
result = result
.iter()
.flat_map(|s| s.split(sep).map(String::from))
.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| s.split_whitespace().map(String::from))
.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 = result
.into_iter()
.filter(|s| seen.insert(s.clone()))
.collect();
}
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(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
} else {
result.sort();
}
}
if flags.sort_reverse {
result.reverse();
}
for _ in 0..flags.quote_level {
result = result
.iter()
.map(|s| format!("'{}'", s.replace('\'', "'\\''")))
.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.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 let Some(width) = flags.pad_left {
let fill = flags.pad_char.unwrap_or(' ');
result = result
.iter()
.map(|s| {
if s.len() < width {
format!("{}{}", fill.to_string().repeat(width - s.len()), s)
} else {
s.clone()
}
})
.collect();
}
if let Some(width) = flags.pad_right {
let fill = flags.pad_char.unwrap_or(' ');
result = result
.iter()
.map(|s| {
if s.len() < width {
format!("{}{}", s, fill.to_string().repeat(width - s.len()))
} else {
s.clone()
}
})
.collect();
}
result
}
fn apply_operator(
var_name: &str,
value: Vec<String>,
operator: Option<&str>,
operand: &str,
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)
{
vec![operand.to_string()]
} else {
value
}
}
Some(":=") | Some("=") => {
if (operator == Some(":=") && (is_empty || !is_set))
|| (operator == Some("=") && !is_set)
{
state
.variables
.insert(var_name.to_string(), operand.to_string());
vec![operand.to_string()]
} else {
value
}
}
Some(":+") | Some("+") => {
if (operator == Some(":+") && !is_empty && is_set) || (operator == Some("+") && is_set)
{
vec![operand.to_string()]
} 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 parts: Vec<&str> = operand.split(':').collect();
let offset: i64 = parts.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
let length: Option<i64> = parts.get(1).and_then(|s| s.parse().ok());
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| remove_prefix(s, operand, false))
.collect()
}
Some("##") => {
value
.iter()
.map(|s| remove_prefix(s, operand, true))
.collect()
}
Some("%") => {
value
.iter()
.map(|s| remove_suffix(s, operand, false))
.collect()
}
Some("%%") => {
value
.iter()
.map(|s| remove_suffix(s, operand, true))
.collect()
}
Some("/") => {
let parts: Vec<&str> = operand.splitn(2, '/').collect();
let pattern = parts.get(0).unwrap_or(&"");
let replacement = parts.get(1).unwrap_or(&"");
value
.iter()
.map(|s| s.replacen(pattern, replacement, 1))
.collect()
}
Some("//") => {
let parts: Vec<&str> = operand.splitn(2, '/').collect();
let pattern = parts.get(0).unwrap_or(&"");
let replacement = parts.get(1).unwrap_or(&"");
value
.iter()
.map(|s| s.replace(pattern, replacement))
.collect()
}
Some("^") => {
value
.iter()
.map(|s| {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().chain(chars).collect(),
None => String::new(),
}
})
.collect()
}
Some("^^") => {
value.iter().map(|s| s.to_uppercase()).collect()
}
Some(",") => {
value
.iter()
.map(|s| {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_lowercase().chain(chars).collect(),
None => String::new(),
}
})
.collect()
}
Some(",,") => {
value.iter().map(|s| s.to_lowercase()).collect()
}
_ => value,
}
}
fn remove_prefix(s: &str, pattern: &str, greedy: bool) -> String {
if pattern == "*" {
return String::new();
}
if pattern.ends_with('*') {
let prefix = &pattern[..pattern.len() - 1];
if s.starts_with(prefix) {
if greedy {
for i in (prefix.len()..=s.len()).rev() {
return s[i..].to_string();
}
} else {
return s[prefix.len()..].to_string();
}
}
} else if s.starts_with(pattern) {
return s[pattern.len()..].to_string();
}
s.to_string()
}
fn remove_suffix(s: &str, pattern: &str, greedy: bool) -> String {
if pattern == "*" {
return String::new();
}
if pattern.starts_with('*') {
let suffix = &pattern[1..];
if s.ends_with(suffix) {
if greedy {
for i in 0..=s.len().saturating_sub(suffix.len()) {
return s[..i].to_string();
}
} else {
return s[..s.len() - suffix.len()].to_string();
}
}
} else if s.ends_with(pattern) {
return s[..s.len() - pattern.len()].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 {
s.contains('{') && s.contains('}')
}
fn xpandbraces(list: &mut LinkList, node_idx: &mut usize) {
let data = match list.get_data(*node_idx) {
Some(d) => d.to_string(),
None => return,
};
if let Some(start) = data.find('{') {
if let Some(end) = data[start..].find('}') {
let prefix = &data[..start];
let content = &data[start + 1..start + end];
let suffix = &data[start + end + 1..];
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 remnulargs(s: &str) -> String {
s.chars().filter(|&c| c != NULARG).collect()
}
fn filesub(s: &str, _flags: u32, _state: &mut SubstState) -> String {
if s.starts_with('~') {
let rest = &s[1..];
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 {
if let Ok(n) = expr.parse::<i64>() {
return n.to_string();
}
if let Some(pos) = expr.find('+') {
if let (Ok(a), Ok(b)) = (
expr[..pos].trim().parse::<i64>(),
expr[pos + 1..].trim().parse::<i64>(),
) {
return (a + b).to_string();
}
}
"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 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 capitalize_next = true;
for c in s.chars() {
if c.is_whitespace() {
capitalize_next = true;
result.push(c);
} else if capitalize_next {
result.extend(c.to_uppercase());
capitalize_next = 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 = modifiers.chars().peekable();
while chars.peek() == Some(&':') {
chars.next();
let mut gbal = false;
let mut wall = false;
let mut sep = 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 s: String = chars.by_ref().take_while(|&c| c != ':').collect();
sep = Some(s);
}
}
_ => break,
}
}
let modifier = match chars.next() {
Some(c) => c,
None => break,
};
if wall {
let separator = sep.as_deref().unwrap_or(" ");
let words: Vec<&str> = result.split(separator).collect();
let modified: Vec<String> = words
.iter()
.map(|w| apply_single_modifier(w, modifier, gbal, state))
.collect();
result = modified.join(separator);
} else {
result = apply_single_modifier(&result, modifier, gbal, state);
}
}
result
}
fn apply_single_modifier(s: &str, modifier: char, gbal: bool, _state: &mut SubstState) -> String {
match modifier {
'a' => {
if s.starts_with('/') {
s.to_string()
} else if let Ok(cwd) = std::env::current_dir() {
format!("{}/{}", cwd.display(), s)
} else {
s.to_string()
}
}
'A' => match std::fs::canonicalize(s) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => s.to_string(),
},
'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 full;
}
}
}
s.to_string()
}
'h' => match s.rfind('/') {
Some(0) => "/".to_string(),
Some(pos) => s[..pos].to_string(),
None => ".".to_string(),
},
't' => match s.rfind('/') {
Some(pos) => s[pos + 1..].to_string(),
None => s.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' => {
format!("'{}'", s.replace('\'', "'\\''"))
}
'Q' => {
let trimmed = s.trim();
if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
|| (trimmed.starts_with('"') && trimmed.ends_with('"'))
{
trimmed[1..trimmed.len() - 1].to_string()
} else {
s.to_string()
}
}
'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,
}
}
_ => s.to_string(),
}
}
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 Vec<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(|a, b| a.to_lowercase().cmp(&b.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_else(|| 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(|c| token_to_char(c)).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;
code >= 0x80 && code <= 0x9F
}
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 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 Vec<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<std::collections::HashMap<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: std::collections::HashMap<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 mut result = s.to_string();
for _ in 0..count.max(1) {
if let Some(pos) = result.rfind('/') {
if pos == 0 {
result = "/".to_string();
break;
} else {
result = result[..pos].to_string();
}
} else {
result = ".".to_string();
break;
}
}
result
}
pub fn remlpaths(s: &str, count: usize) -> String {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() <= count {
parts.last().unwrap_or(&"").to_string()
} else {
parts[parts.len() - count..].join("/")
}
}
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 {
if s.starts_with('/') {
s.to_string()
} else if let Ok(cwd) = std::env::current_dir() {
format!("{}/{}", cwd.display(), s)
} else {
s.to_string()
}
}
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 MathResult {
pub fn to_string(&self) -> String {
match self {
MathResult::Integer(n) => n.to_string(),
MathResult::Float(n) => n.to_string(),
}
}
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 ParamValue {
pub fn to_string(&self) -> String {
match self {
ParamValue::Scalar(s) => s.clone(),
ParamValue::Array(arr) => arr.join(" "),
}
}
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)]
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.len() >= 1);
}
#[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$");
}
}
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;
}
if let Some(c) = char::from_u32(val as u32) {
Some(c.to_string())
} else {
None
}
}
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 Vec<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(|c| c == EQUALS || c == '=') {
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
}