implemented most variable parameter expansion builtins

This commit is contained in:
2025-05-12 16:14:06 -04:00
parent 2666670939
commit bbb8162201
4 changed files with 198 additions and 8 deletions

39
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.8.0" version = "2.8.0"
@@ -95,6 +104,7 @@ dependencies = [
"insta", "insta",
"nix", "nix",
"pretty_assertions", "pretty_assertions",
"regex",
"rustyline", "rustyline",
] ]
@@ -241,6 +251,35 @@ dependencies = [
"nibble_vec", "nibble_vec",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"

View File

@@ -15,6 +15,7 @@ glob = "0.3.2"
insta = "1.42.2" insta = "1.42.2"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
regex = "1.11.1"
rustyline = { version = "15.0.0", features = [ "derive" ] } rustyline = { version = "15.0.0", features = [ "derive" ] }
[[bin]] [[bin]]

View File

@@ -2,6 +2,7 @@ use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use glob::Pattern; use glob::Pattern;
use regex::Regex;
use crate::state::{read_vars, write_meta, write_vars, LogTab}; use crate::state::{read_vars, write_meta, write_vars, LogTab};
use crate::procio::{IoBuf, IoFrame, IoMode}; use crate::procio::{IoBuf, IoFrame, IoMode};
@@ -125,11 +126,15 @@ impl Expander {
} }
'{' if var_name.is_empty() => in_brace = true, '{' if var_name.is_empty() => in_brace = true,
'}' if in_brace => { '}' if in_brace => {
flog!(DEBUG, var_name);
let var_val = perform_param_expansion(&var_name)?; let var_val = perform_param_expansion(&var_name)?;
result.push_str(&var_val); result.push_str(&var_val);
var_name.clear(); var_name.clear();
break break
} }
_ if in_brace => {
var_name.push(ch)
}
_ if is_hard_sep(ch) || ch == DUB_QUOTE || ch == SUBSH || ch == '/' => { _ if is_hard_sep(ch) || ch == DUB_QUOTE || ch == SUBSH || ch == '/' => {
let var_val = read_vars(|v| v.get_var(&var_name)); let var_val = read_vars(|v| v.get_var(&var_name));
result.push_str(&var_val); result.push_str(&var_val);
@@ -324,6 +329,7 @@ pub fn unescape_str(raw: &str) -> String {
result result
} }
#[derive(Debug)]
pub enum ParamExp { pub enum ParamExp {
Len, // #var_name Len, // #var_name
DefaultUnsetOrNull(String), // :- DefaultUnsetOrNull(String), // :-
@@ -459,14 +465,17 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
'+' | '+' |
'=' | '=' |
'?' => { '?' => {
rest = chars.collect(); rest.push(ch);
rest.push_str(&chars.collect::<String>());
break break
} }
_ => var_name.push(ch) _ => var_name.push(ch)
} }
} }
flog!(DEBUG,rest);
if let Ok(expansion) = rest.parse::<ParamExp>() { if let Ok(expansion) = rest.parse::<ParamExp>() {
flog!(DEBUG,expansion);
match expansion { match expansion {
ParamExp::Len => unreachable!(), ParamExp::Len => unreachable!(),
ParamExp::DefaultUnsetOrNull(default) => { ParamExp::DefaultUnsetOrNull(default) => {
@@ -559,13 +568,90 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
Ok(value) Ok(value)
} }
ParamExp::RemLongestPrefix(prefix) => todo!(), ParamExp::RemLongestPrefix(prefix) => {
ParamExp::RemShortestSuffix(suffix) => todo!(), let value = vars.get_var(&var_name);
ParamExp::RemLongestSuffix(suffix) => todo!(), let pattern = Pattern::new(&prefix).unwrap();
ParamExp::ReplaceFirstMatch(search, replace) => todo!(), for i in (0..=value.len()).rev() {
ParamExp::ReplaceAllMatches(search, replace) => todo!(), let sliced = &value[..i];
ParamExp::ReplacePrefix(search, replace) => todo!(), if pattern.matches(sliced) {
ParamExp::ReplaceSuffix(search, replace) => todo!(), return Ok(value[i..].to_string());
}
}
Ok(value) // no match
}
ParamExp::RemShortestSuffix(suffix) => {
let value = vars.get_var(&var_name);
let pattern = Pattern::new(&suffix).unwrap();
for i in 0..=value.len() {
let sliced = &value[i..];
if pattern.matches(sliced) {
return Ok(value[..i].to_string());
}
}
Ok(value)
}
ParamExp::RemLongestSuffix(suffix) => {
let value = vars.get_var(&var_name);
let pattern = Pattern::new(&suffix).unwrap();
for i in (0..=value.len()).rev() {
let sliced = &value[i..];
if pattern.matches(sliced) {
return Ok(value[..i].to_string());
}
}
Ok(value)
}
ParamExp::ReplaceFirstMatch(search, replace) => {
let value = vars.get_var(&var_name);
let regex = glob_to_regex(&search, false); // unanchored pattern
if let Some(mat) = regex.find(&value) {
let before = &value[..mat.start()];
let after = &value[mat.end()..];
let result = format!("{}{}{}", before, replace, after);
Ok(result)
} else {
Ok(value)
}
}
ParamExp::ReplaceAllMatches(search, replace) => {
let value = vars.get_var(&var_name);
let regex = glob_to_regex(&search, false);
let mut result = String::new();
let mut last_match_end = 0;
for mat in regex.find_iter(&value) {
result.push_str(&value[last_match_end..mat.start()]);
result.push_str(&replace);
last_match_end = mat.end();
}
// Append the rest of the string
result.push_str(&value[last_match_end..]);
Ok(result)
}
ParamExp::ReplacePrefix(search, replace) => {
let value = vars.get_var(&var_name);
let pattern = Pattern::new(&search).unwrap();
for i in (0..=value.len()).rev() {
let sliced = &value[..i];
if pattern.matches(sliced) {
return Ok(format!("{}{}",replace,&value[i..]))
}
}
Ok(value)
}
ParamExp::ReplaceSuffix(search, replace) => {
let value = vars.get_var(&var_name);
let pattern = Pattern::new(&search).unwrap();
for i in (0..=value.len()).rev() {
let sliced = &value[i..];
if pattern.matches(sliced) {
return Ok(format!("{}{}",&value[..i],replace))
}
}
Ok(value)
}
ParamExp::VarNamesWithSuffix(suffix) => todo!(), ParamExp::VarNamesWithSuffix(suffix) => todo!(),
ParamExp::ExpandInnerVar(var_name) => todo!(), ParamExp::ExpandInnerVar(var_name) => todo!(),
} }
@@ -574,6 +660,43 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
} }
fn glob_to_regex(glob: &str, anchored: bool) -> Regex {
let mut regex = String::new();
if anchored {
regex.push('^');
}
for ch in glob.chars() {
match ch {
'*' => regex.push_str(".*"),
'?' => regex.push('.'),
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '[' | ']' | '{' | '}' | '\\' => {
regex.push('\\');
regex.push(ch);
}
_ => regex.push(ch),
}
}
if anchored {
regex.push('$');
}
Regex::new(&regex).unwrap()
}
fn glob_to_regex_unanchored(glob: &str) -> Regex {
let mut regex = String::new();
for ch in glob.chars() {
match ch {
'*' => regex.push_str(".*"),
'?' => regex.push('.'),
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '[' | ']' | '{' | '}' | '\\' => {
regex.push('\\');
regex.push(ch);
}
_ => regex.push(ch),
}
}
Regex::new(&regex).unwrap()
}
#[derive(Debug)] #[derive(Debug)]
pub enum PromptTk { pub enum PromptTk {
AsciiOct(i32), AsciiOct(i32),

View File

@@ -339,6 +339,33 @@ impl LexStream {
pos += ch.len_utf8(); pos += ch.len_utf8();
} }
} }
'$' if chars.peek() == Some(&'{') => {
pos += 2;
chars.next();
let mut brace_count = 0;
while let Some(brc_ch) = chars.next() {
match brc_ch {
'\\' => {
pos += 1;
if let Some(next_ch) = chars.next() {
pos += next_ch.len_utf8()
}
}
'{' => {
pos += 1;
brace_count += 1;
}
'}' => {
pos += 1;
brace_count -= 1;
if brace_count == 0 {
break
}
}
_ => pos += ch.len_utf8()
}
}
}
'$' if chars.peek() == Some(&'(') => { '$' if chars.peek() == Some(&'(') => {
pos += 2; pos += 2;
chars.next(); chars.next();