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:
336
src/expand.rs
336
src/expand.rs
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user