From 1250d541dcc63c26fa8d81c7a45ed06c1d033e9d Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Mon, 12 May 2025 01:04:53 -0400 Subject: [PATCH] began work on implementing those weird variable string op things --- src/expand.rs | 257 +++++++++++++++++++++++++++++++++++++++++++++++++- src/state.rs | 57 ++++++----- 2 files changed, 289 insertions(+), 25 deletions(-) diff --git a/src/expand.rs b/src/expand.rs index 853e8e8..2d41789 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,6 +1,9 @@ use std::collections::HashSet; +use std::str::FromStr; -use crate::state::{read_vars, write_meta, LogTab}; +use glob::Pattern; + +use crate::state::{read_vars, write_meta, write_vars, LogTab}; use crate::procio::{IoBuf, IoFrame, IoMode}; use crate::prelude::*; use crate::parse::{Redir, RedirType}; @@ -122,7 +125,7 @@ impl Expander { } '{' if var_name.is_empty() => in_brace = true, '}' if in_brace => { - let var_val = read_vars(|v| v.get_var(&var_name)); + let var_val = perform_param_expansion(&var_name)?; result.push_str(&var_val); var_name.clear(); break @@ -321,6 +324,256 @@ pub fn unescape_str(raw: &str) -> String { result } +pub enum ParamExp { + Len, // #var_name + DefaultUnsetOrNull(String), // :- + DefaultUnset(String), // - + SetDefaultUnsetOrNull(String), // := + SetDefaultUnset(String), // = + AltSetNotNull(String), // :+ + AltNotNull(String), // + + ErrUnsetOrNull(String), // :? + ErrUnset(String), // ? + Substr(usize), // :pos + SubstrLen(usize,usize), // :pos:len + RemShortestPrefix(String), // #pattern + RemLongestPrefix(String), // ##pattern + RemShortestSuffix(String), // %pattern + RemLongestSuffix(String), // %%pattern + ReplaceFirstMatch(String,String), // /search/replace + ReplaceAllMatches(String,String), // //search/replace + ReplacePrefix(String,String), // #search/replace + ReplaceSuffix(String,String), // %search/replace + VarNamesWithSuffix(String), // !prefix@ || !prefix* + ExpandInnerVar(String), // !var +} + +impl FromStr for ParamExp { + type Err = ShErr; + + fn from_str(s: &str) -> Result { + use ParamExp::*; + + let parse_err = || Err(ShErr::Simple { + kind: ShErrKind::SyntaxErr, + msg: "Invalid parameter expansion".into(), + notes: vec![], + }); + + // Handle indirect var expansion: ${!var} + if let Some(var) = s.strip_prefix('!') { + if var.ends_with('*') || var.ends_with('@') { + return Ok(VarNamesWithSuffix(var.to_string())); + } + return Ok(ExpandInnerVar(var.to_string())); + } + + // Pattern removals + if let Some(rest) = s.strip_prefix("##") { + return Ok(RemLongestPrefix(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('#') { + return Ok(RemShortestPrefix(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("%%") { + return Ok(RemLongestSuffix(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('%') { + return Ok(RemShortestSuffix(rest.to_string())); + } + + // Replacements + if let Some(rest) = s.strip_prefix("//") { + let mut parts = rest.splitn(2, '/'); + let pattern = parts.next().unwrap_or(""); + let repl = parts.next().unwrap_or(""); + return Ok(ReplaceAllMatches(pattern.to_string(), repl.to_string())); + } + if let Some(rest) = s.strip_prefix('/') { + let mut parts = rest.splitn(2, '/'); + let pattern = parts.next().unwrap_or(""); + let repl = parts.next().unwrap_or(""); + return Ok(ReplaceFirstMatch(pattern.to_string(), repl.to_string())); + } + + // Fallback / assignment / alt + if let Some(rest) = s.strip_prefix(":-") { + return Ok(DefaultUnsetOrNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('-') { + return Ok(DefaultUnset(rest.to_string())); + } else if let Some(rest) = s.strip_prefix(":+") { + return Ok(AltSetNotNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('+') { + return Ok(AltNotNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix(":=") { + return Ok(SetDefaultUnsetOrNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('=') { + return Ok(SetDefaultUnset(rest.to_string())); + } else if let Some(rest) = s.strip_prefix(":?") { + return Ok(ErrUnsetOrNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('?') { + return Ok(ErrUnset(rest.to_string())); + } + + // Substring + if let Some((pos, len)) = parse_pos_len(s) { + return Ok(match len { + Some(l) => SubstrLen(pos, l), + None => Substr(pos), + }); + } + + parse_err() + } +} + +pub fn parse_pos_len(s: &str) -> Option<(usize, Option)> { + let raw = s.strip_prefix(':')?; + if let Some((start,len)) = raw.split_once(':') { + Some(( + start.parse::().ok()?, + len.parse::().ok(), + )) + } else { + Some(( + raw.parse::().ok()?, + None, + )) + } +} + +pub fn perform_param_expansion(raw: &str) -> ShResult { + let vars = read_vars(|v| v.clone()); + let mut chars = raw.chars(); + let mut var_name = String::new(); + let mut rest = String::new(); + if raw.starts_with('#') { + return Ok(vars.get_var(raw.strip_prefix('#').unwrap()).len().to_string()) + } + + while let Some(ch) = chars.next() { + match ch { + '!' | + '#' | + '%' | + ':' | + '-' | + '+' | + '=' | + '?' => { + rest = chars.collect(); + break + } + _ => var_name.push(ch) + } + } + + if let Ok(expansion) = rest.parse::() { + match expansion { + ParamExp::Len => unreachable!(), + ParamExp::DefaultUnsetOrNull(default) => { + if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::DefaultUnset(default) => { + if !vars.var_exists(&var_name) { + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::SetDefaultUnsetOrNull(default) => { + if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + write_vars(|v| v.set_var(&var_name, &default, false)); + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::SetDefaultUnset(default) => { + if !vars.var_exists(&var_name) { + write_vars(|v| v.set_var(&var_name, &default, false)); + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::AltSetNotNull(alt) => { + if vars.var_exists(&var_name) && !vars.get_var(&var_name).is_empty() { + Ok(alt) + } else { + Ok("".into()) + } + } + ParamExp::AltNotNull(alt) => { + if vars.var_exists(&var_name) { + Ok(alt) + } else { + Ok("".into()) + } + } + ParamExp::ErrUnsetOrNull(err) => { + if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + Err( + ShErr::Simple { kind: ShErrKind::ExecFail, msg: err, notes: vec![] } + ) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::ErrUnset(err) => { + if !vars.var_exists(&var_name) { + Err( + ShErr::Simple { kind: ShErrKind::ExecFail, msg: err, notes: vec![] } + ) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::Substr(pos) => { + let value = vars.get_var(&var_name); + if let Some(substr) = value.get(pos..) { + Ok(substr.to_string()) + } else { + Ok(value) + } + } + ParamExp::SubstrLen(pos, len) => { + let value = vars.get_var(&var_name); + let end = pos.saturating_add(len); + if let Some(substr) = value.get(pos..end) { + Ok(substr.to_string()) + } else { + Ok(value) + } + } + ParamExp::RemShortestPrefix(prefix) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&prefix).unwrap(); + for i in 0..=value.len() { + let sliced = &value[..i]; + if pattern.matches(sliced) { + return Ok(value[i..].to_string()) + } + } + Ok(value) + } + ParamExp::RemLongestPrefix(prefix) => todo!(), + ParamExp::RemShortestSuffix(suffix) => todo!(), + ParamExp::RemLongestSuffix(suffix) => todo!(), + ParamExp::ReplaceFirstMatch(search, replace) => todo!(), + ParamExp::ReplaceAllMatches(search, replace) => todo!(), + ParamExp::ReplacePrefix(search, replace) => todo!(), + ParamExp::ReplaceSuffix(search, replace) => todo!(), + ParamExp::VarNamesWithSuffix(suffix) => todo!(), + ParamExp::ExpandInnerVar(var_name) => todo!(), + } + } else { + Ok(vars.get_var(&var_name)) + } +} + #[derive(Debug)] pub enum PromptTk { AsciiOct(i32), diff --git a/src/state.rs b/src/state.rs index 7dcfc9e..5c0882d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, VecDeque}, ops::Deref, sync::{LazyLock, RwLock, use nix::unistd::{gethostname, getppid, User}; -use crate::{exec_input, jobs::JobTab, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt}, parse::{lex::get_char, ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts}; +use crate::{exec_input, jobs::JobTab, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts}; pub static JOB_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(JobTab::new())); @@ -118,7 +118,7 @@ impl Deref for Var { #[derive(Default,Clone,Debug)] pub struct VarTab { vars: HashMap, - params: HashMap, + params: HashMap, sh_argv: VecDeque, // Using a VecDeque makes the implementation of `shift` straightforward } @@ -131,13 +131,13 @@ impl VarTab { var_tab.init_sh_argv(); var_tab } - fn init_params() -> HashMap { + fn init_params() -> HashMap { let mut params = HashMap::new(); - params.insert('?', "0".into()); // Last command exit status - params.insert('#', "0".into()); // Number of positional parameters - params.insert('0', std::env::current_exe().unwrap().to_str().unwrap().to_string()); // Name of the shell - params.insert('$', Pid::this().to_string()); // PID of the shell - params.insert('!', "".into()); // PID of the last background job (if any) + params.insert("?".into(), "0".into()); // Last command exit status + params.insert("#".into(), "0".into()); // Number of positional parameters + params.insert("0".into(), std::env::current_exe().unwrap().to_str().unwrap().to_string()); // Name of the shell + params.insert("$".into(), Pid::this().to_string()); // PID of the shell + params.insert("!".into(), "".into()); // PID of the last background job (if any) params } fn init_env() { @@ -214,8 +214,8 @@ impl VarTab { self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string()); } fn update_arg_params(&mut self) { - self.set_param('@', &self.sh_argv.clone().to_vec()[1..].join(" ")); - self.set_param('#', &(self.sh_argv.len() - 1).to_string()); + self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" ")); + self.set_param("#", &(self.sh_argv.len() - 1).to_string()); } /// Push an arg to the front of the arg deque pub fn fpush_arg(&mut self, arg: String) { @@ -245,10 +245,10 @@ impl VarTab { pub fn vars_mut(&mut self) -> &mut HashMap { &mut self.vars } - pub fn params(&self) -> &HashMap { + pub fn params(&self) -> &HashMap { &self.params } - pub fn params_mut(&mut self) -> &mut HashMap { + pub fn params_mut(&mut self) -> &mut HashMap { &mut self.params } pub fn export_var(&mut self, var_name: &str) { @@ -258,8 +258,9 @@ impl VarTab { } } pub fn get_var(&self, var: &str) -> String { - if var.chars().count() == 1 { - let param = self.get_param(get_char(var, 0).unwrap()); + if var.chars().count() == 1 || + var.parse::().is_ok() { + let param = self.get_param(var); if !param.is_empty() { return param } @@ -285,21 +286,31 @@ impl VarTab { self.vars.insert(var_name.to_string(), var); } } - pub fn set_param(&mut self, param: char, val: &str) { - self.params.insert(param,val.to_string()); + pub fn var_exists(&self, var_name: &str) -> bool { + if var_name.parse::().is_ok() { + return self.params.contains_key(var_name); + } + self.vars.contains_key(var_name) || + ( + var_name.len() == 1 && + self.params.contains_key(var_name) + ) } - pub fn get_param(&self, param: char) -> String { - if param.is_ascii_digit() { + pub fn set_param(&mut self, param: &str, val: &str) { + self.params.insert(param.to_string(),val.to_string()); + } + pub fn get_param(&self, param: &str) -> String { + if param.parse::().is_ok() { let argv_idx = param .to_string() .parse::() .unwrap(); let arg = self.sh_argv.get(argv_idx).map(|s| s.to_string()).unwrap_or_default(); arg - } else if param == '?' { - self.params.get(¶m).map(|s| s.to_string()).unwrap_or("0".into()) + } else if param == "?" { + self.params.get(param).map(|s| s.to_string()).unwrap_or("0".into()) } else { - self.params.get(¶m).map(|s| s.to_string()).unwrap_or_default() + self.params.get(param).map(|s| s.to_string()).unwrap_or_default() } } } @@ -389,11 +400,11 @@ pub fn get_shopt(path: &str) -> String { } pub fn get_status() -> i32 { - read_vars(|v| v.get_param('?')).parse::().unwrap() + read_vars(|v| v.get_param("?")).parse::().unwrap() } #[track_caller] pub fn set_status(code: i32) { - write_vars(|v| v.set_param('?', &code.to_string())) + write_vars(|v| v.set_param("?", &code.to_string())) } /// Save the current state of the logic and variable table, and the working directory path