Implemented proper variable scoping

Extracted business logic out of signal handler functions

Consolidated state variables into a single struct

Implemented var types
This commit is contained in:
2026-01-28 19:30:48 -05:00
parent 8ad53f09b3
commit ad0e4277cb
17 changed files with 2154 additions and 1127 deletions

View File

@@ -1,5 +1,6 @@
use std::collections::HashSet;
use std::iter::Peekable;
use std::mem::take;
use std::str::{Chars, FromStr};
use glob::Pattern;
@@ -11,7 +12,7 @@ use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFl
use crate::parse::{Redir, RedirType};
use crate::prelude::*;
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::state::{read_jobs, read_vars, write_jobs, write_meta, write_vars, LogTab};
use crate::state::{LogTab, VarFlags, read_jobs, read_vars, write_jobs, write_meta, write_vars};
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
@@ -40,7 +41,7 @@ impl Tk {
pub fn expand(self) -> ShResult<Self> {
let flags = self.flags;
let span = self.span.clone();
let exp = Expander::new(self).expand()?;
let exp = Expander::new(self)?.expand()?;
let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags })
}
@@ -58,9 +59,11 @@ pub struct Expander {
}
impl Expander {
pub fn new(raw: Tk) -> Self {
let unescaped = unescape_str(raw.span.as_str());
Self { raw: unescaped }
pub fn new(raw: Tk) -> ShResult<Self> {
let mut raw = raw.span.as_str().to_string();
raw = expand_braces_full(&raw)?.join(" ");
let unescaped = unescape_str(&raw);
Ok(Self { raw: unescaped })
}
pub fn expand(&mut self) -> ShResult<Vec<String>> {
let mut chars = self.raw.chars().peekable();
@@ -100,6 +103,323 @@ impl Expander {
}
}
/// Check if a string contains valid brace expansion patterns.
/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost level.
fn has_braces(s: &str) -> bool {
let mut chars = s.chars().peekable();
let mut depth = 0;
let mut found_open = false;
let mut has_comma = false;
let mut has_range = false;
let mut cur_quote: Option<char> = None;
while let Some(ch) = chars.next() {
match ch {
'\\' => { chars.next(); } // skip escaped char
'\'' if cur_quote.is_none() => cur_quote = Some('\''),
'\'' if cur_quote == Some('\'') => cur_quote = None,
'"' if cur_quote.is_none() => cur_quote = Some('"'),
'"' if cur_quote == Some('"') => cur_quote = None,
'{' if cur_quote.is_none() => {
if depth == 0 {
found_open = true;
has_comma = false;
has_range = false;
}
depth += 1;
}
'}' if cur_quote.is_none() && depth > 0 => {
depth -= 1;
if depth == 0 && found_open && (has_comma || has_range) {
return true;
}
}
',' if cur_quote.is_none() && depth == 1 => {
has_comma = true;
}
'.' if cur_quote.is_none() && depth == 1 => {
if chars.peek() == Some(&'.') {
chars.next();
has_range = true;
}
}
_ => {}
}
}
false
}
/// Expand braces in a string, zsh-style: one level per call, loop until done.
/// Returns a Vec of expanded strings.
fn expand_braces_full(input: &str) -> ShResult<Vec<String>> {
let mut results = vec![input.to_string()];
// Keep expanding until no results contain braces
loop {
let mut any_expanded = false;
let mut new_results = Vec::new();
for word in results {
if has_braces(&word) {
any_expanded = true;
let expanded = expand_one_brace(&word)?;
new_results.extend(expanded);
} else {
new_results.push(word);
}
}
results = new_results;
if !any_expanded {
break;
}
}
Ok(results)
}
/// Expand the first (outermost) brace expression in a word.
/// "pre{a,b}post" -> ["preapost", "prebpost"]
/// "pre{1..3}post" -> ["pre1post", "pre2post", "pre3post"]
fn expand_one_brace(word: &str) -> ShResult<Vec<String>> {
let (prefix, inner, suffix) = match get_brace_parts(word) {
Some(parts) => parts,
None => return Ok(vec![word.to_string()]), // No valid braces
};
// Split the inner content on top-level commas, or expand as range
let parts = split_brace_inner(&inner);
// If we got back a single part with no expansion, treat as literal
if parts.len() == 1 && parts[0] == inner {
// Check if it's a range
if let Some(range_parts) = try_expand_range(&inner) {
return Ok(range_parts
.into_iter()
.map(|p| format!("{}{}{}", prefix, p, suffix))
.collect());
}
// Not a valid brace expression, return as-is with literal braces
return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]);
}
Ok(parts
.into_iter()
.map(|p| format!("{}{}{}", prefix, p, suffix))
.collect())
}
/// Extract prefix, inner, and suffix from a brace expression.
/// "pre{a,b}post" -> Some(("pre", "a,b", "post"))
fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
let mut chars = word.chars().enumerate().peekable();
let mut prefix = String::new();
let mut cur_quote: Option<char> = None;
let mut brace_start = None;
// Find the opening brace
while let Some((i, ch)) = chars.next() {
match ch {
'\\' => {
prefix.push(ch);
if let Some((_, next)) = chars.next() {
prefix.push(next);
}
}
'\'' if cur_quote.is_none() => { cur_quote = Some('\'');
prefix.push(ch); }
'\'' if cur_quote == Some('\'') => { cur_quote = None;
prefix.push(ch); }
'"' if cur_quote.is_none() => { cur_quote = Some('"'); prefix.push(ch); }
'"' if cur_quote == Some('"') => { cur_quote = None; prefix.push(ch); }
'{' if cur_quote.is_none() => {
brace_start = Some(i);
break;
}
_ => prefix.push(ch),
}
}
let brace_start = brace_start?;
// Find matching closing brace
let mut depth = 1;
let mut inner = String::new();
cur_quote = None;
while let Some((_, ch)) = chars.next() {
match ch {
'\\' => {
inner.push(ch);
if let Some((_, next)) = chars.next() {
inner.push(next);
}
}
'\'' if cur_quote.is_none() => { cur_quote = Some('\''); inner.push(ch); }
'\'' if cur_quote == Some('\'') => { cur_quote = None; inner.push(ch); }
'"' if cur_quote.is_none() => { cur_quote = Some('"'); inner.push(ch); }
'"' if cur_quote == Some('"') => { cur_quote = None; inner.push(ch); }
'{' if cur_quote.is_none() => {
depth += 1;
inner.push(ch);
}
'}' if cur_quote.is_none() => {
depth -= 1;
if depth == 0 {
break;
}
inner.push(ch);
}
_ => inner.push(ch),
}
}
if depth != 0 {
return None; // Unbalanced braces
}
// Collect suffix
let suffix: String = chars.map(|(_, c)| c).collect();
Some((prefix, inner, suffix))
}
/// Split brace inner content on top-level commas.
/// "a,b,c" -> ["a", "b", "c"]
/// "a,{b,c},d" -> ["a", "{b,c}", "d"]
fn split_brace_inner(inner: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut chars = inner.chars().peekable();
let mut depth = 0;
let mut cur_quote: Option<char> = None;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
current.push(ch);
if let Some(next) = chars.next() {
current.push(next);
}
}
'\'' if cur_quote.is_none() => { cur_quote = Some('\''); current.push(ch); }
'\'' if cur_quote == Some('\'') => { cur_quote = None; current.push(ch); }
'"' if cur_quote.is_none() => { cur_quote = Some('"'); current.push(ch); }
'"' if cur_quote == Some('"') => { cur_quote = None; current.push(ch); }
'{' if cur_quote.is_none() => {
depth += 1;
current.push(ch);
}
'}' if cur_quote.is_none() => {
depth -= 1;
current.push(ch);
}
',' if cur_quote.is_none() && depth == 0 => {
parts.push(std::mem::take(&mut current));
}
_ => current.push(ch),
}
}
parts.push(current);
parts
}
/// Try to expand a range like "1..5" or "a..z" or "1..10..2"
fn try_expand_range(inner: &str) -> Option<Vec<String>> {
// Look for ".." pattern
let parts: Vec<&str> = inner.split("..").collect();
match parts.len() {
2 => {
let start = parts[0];
let end = parts[1];
expand_range(start, end, 1)
}
3 => {
let start = parts[0];
let end = parts[1];
let step: i32 = parts[2].parse().ok()?;
if step == 0 { return None; }
expand_range(start, end, step.unsigned_abs() as usize)
}
_ => None,
}
}
fn expand_range(start: &str, end: &str, step: usize) ->
Option<Vec<String>> {
// Try character range first
if is_alpha_range_bound(start) && is_alpha_range_bound(end) {
let start_char = start.chars().next()? as u8;
let end_char = end.chars().next()? as u8;
let reverse = end_char < start_char;
let (lo, hi) = if reverse {
(end_char, start_char)
} else {
(start_char, end_char)
};
let chars: Vec<String> = (lo..=hi)
.step_by(step)
.map(|c| (c as char).to_string())
.collect();
return Some(if reverse {
chars.into_iter().rev().collect()
} else {
chars
});
}
// Try numeric range
if is_numeric_range_bound(start) && is_numeric_range_bound(end) {
let start_num: i32 = start.parse().ok()?;
let end_num: i32 = end.parse().ok()?;
let reverse = end_num < start_num;
// Handle zero-padding
let pad_width = start.len().max(end.len());
let needs_padding = start.starts_with('0') ||
end.starts_with('0');
let (lo, hi) = if reverse {
(end_num, start_num)
} else {
(start_num, end_num)
};
let nums: Vec<String> = (lo..=hi)
.step_by(step)
.map(|n| {
if needs_padding {
format!("{:0>width$}", n, width = pad_width)
} else {
n.to_string()
}
})
.collect();
return Some(if reverse {
nums.into_iter().rev().collect()
} else {
nums
});
}
None
}
fn is_alpha_range_bound(word: &str) -> bool {
word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic())
}
fn is_numeric_range_bound(word: &str) -> bool {
!word.is_empty() && word.chars().all(|c| c.is_ascii_digit())
}
pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let mut result = String::new();
@@ -897,7 +1217,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
}
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));
write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE));
Ok(default)
} else {
Ok(vars.get_var(&var_name))
@@ -905,7 +1225,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
}
ParamExp::SetDefaultUnset(default) => {
if !vars.var_exists(&var_name) {
write_vars(|v| v.set_var(&var_name, &default, false));
write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE));
Ok(default)
} else {
Ok(vars.get_var(&var_name))
@@ -1061,7 +1381,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
}
ParamExp::VarNamesWithPrefix(prefix) => {
let mut match_vars = vec![];
for var in vars.vars().keys() {
for var in vars.flatten_vars().keys() {
if var.starts_with(&prefix) {
match_vars.push(var.clone())
}