use super::state::ShellState;
fn glob_match(pattern: &str, text: &str) -> bool {
glob_match_recursive(pattern, text, 0, 0)
}
fn glob_match_recursive(pattern: &str, text: &str, pi: usize, ti: usize) -> bool {
if pi >= pattern.len() {
return ti >= text.len();
}
if ti >= text.len() {
return pattern[pi..].chars().all(|c| c == '*');
}
match pattern.chars().nth(pi).unwrap() {
'*' => {
if glob_match_recursive(pattern, text, pi + 1, ti) {
return true;
}
if ti < text.len() {
return glob_match_recursive(pattern, text, pi, ti + 1);
}
false
}
c => {
if c == text.chars().nth(ti).unwrap() {
glob_match_recursive(pattern, text, pi + 1, ti + 1)
} else {
false
}
}
}
}
fn find_shortest_prefix_match(pattern: &str, text: &str) -> Option<usize> {
if pattern.is_empty() {
return Some(0);
}
for i in 0..=text.len() {
let prefix = &text[..i];
if glob_match(pattern, prefix) {
return Some(i);
}
}
None
}
fn find_longest_prefix_match(pattern: &str, text: &str) -> Option<usize> {
if pattern.is_empty() {
return Some(0);
}
let mut longest = None;
for i in 0..=text.len() {
let prefix = &text[..i];
if glob_match(pattern, prefix) {
longest = Some(i);
}
}
longest
}
fn find_shortest_suffix_match(pattern: &str, text: &str) -> Option<usize> {
if pattern.is_empty() {
return Some(text.len());
}
for i in (0..=text.len()).rev() {
let suffix = &text[i..];
if glob_match(pattern, suffix) {
return Some(i);
}
}
None
}
fn find_longest_suffix_match(pattern: &str, text: &str) -> Option<usize> {
if pattern.is_empty() {
return Some(text.len());
}
let mut longest = None;
for i in (0..=text.len()).rev() {
let suffix = &text[i..];
if glob_match(pattern, suffix) {
longest = Some(i);
}
}
longest
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParameterModifier {
None,
Default(String),
AssignDefault(String),
Alternative(String),
Error(String),
Substring(usize),
SubstringWithLength(usize, usize),
RemoveShortestPrefix(String),
RemoveLongestPrefix(String),
RemoveShortestSuffix(String),
RemoveLongestSuffix(String),
Substitute(String, String),
SubstituteAll(String, String),
Indirect,
IndirectPrefix,
IndirectPrefixAt,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParameterExpansion {
pub var_name: String,
pub modifier: ParameterModifier,
}
pub fn parse_parameter_expansion(content: &str) -> Result<ParameterExpansion, String> {
if content.is_empty() {
return Err("Empty parameter expansion".to_string());
}
let chars = content.chars();
let mut var_name = String::new();
for ch in chars {
if ch == ':' || ch == '#' || ch == '%' || ch == '/' {
let modifier_str: String = content[var_name.len()..].to_string();
let modifier = parse_modifier(&modifier_str)?;
return Ok(ParameterExpansion { var_name, modifier });
} else if ch == '!' {
var_name.push(ch);
} else if ch.is_alphanumeric() || ch == '_' || ch == '*' {
var_name.push(ch);
} else {
return Err(format!("Invalid character '{}' in variable name", ch));
}
}
let (final_var_name, modifier) = if let Some(stripped) = var_name.strip_prefix('!') {
if let Some(prefix_var) = stripped.strip_suffix('*') {
(prefix_var.to_string(), ParameterModifier::IndirectPrefix)
} else if let Some(prefix_var) = stripped.strip_suffix('@') {
(prefix_var.to_string(), ParameterModifier::IndirectPrefixAt)
} else {
(stripped.to_string(), ParameterModifier::Indirect)
}
} else {
(var_name, ParameterModifier::None)
};
Ok(ParameterExpansion {
var_name: final_var_name,
modifier,
})
}
fn parse_modifier(modifier_str: &str) -> Result<ParameterModifier, String> {
if modifier_str.is_empty() {
return Ok(ParameterModifier::None);
}
let mut chars = modifier_str.chars();
match chars.next().unwrap() {
':' => {
match chars.next() {
Some('=') => {
let word = modifier_str[2..].to_string();
Ok(ParameterModifier::AssignDefault(word))
}
Some('-') => {
let word = modifier_str[2..].to_string();
Ok(ParameterModifier::Default(word))
}
Some('+') => {
let word = modifier_str[2..].to_string();
Ok(ParameterModifier::Alternative(word))
}
Some('?') => {
let word = modifier_str[2..].to_string();
Ok(ParameterModifier::Error(word))
}
Some(ch) if ch.is_ascii_digit() => {
let after_colon = &modifier_str[1..]; let offset_end = after_colon.find(':').unwrap_or(after_colon.len());
let offset_str = &after_colon[..offset_end];
if offset_str.is_empty() {
return Err("Missing offset in substring operation".to_string());
}
let offset: usize = offset_str.parse().map_err(|_| "Invalid offset number")?;
if offset_end < after_colon.len() {
let after_offset = &after_colon[offset_end + 1..]; if !after_offset.is_empty()
&& after_offset.chars().all(|c| c.is_ascii_digit())
{
let length: usize =
after_offset.parse().map_err(|_| "Invalid length number")?;
Ok(ParameterModifier::SubstringWithLength(offset, length))
} else {
Ok(ParameterModifier::Substring(offset))
}
} else {
Ok(ParameterModifier::Substring(offset))
}
}
_ => Err(format!("Invalid modifier: {}", modifier_str)),
}
}
'#' => {
if let Some(pattern) = modifier_str.strip_prefix("##") {
Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
} else if let Some(pattern) = modifier_str.strip_prefix('#') {
Ok(ParameterModifier::RemoveShortestPrefix(pattern.to_string()))
} else {
Err(format!("Invalid prefix removal modifier: {}", modifier_str))
}
}
'%' => {
if let Some(pattern) = modifier_str.strip_prefix("%%") {
Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
} else if let Some(pattern) = modifier_str.strip_prefix('%') {
Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
} else {
Err(format!("Invalid suffix removal modifier: {}", modifier_str))
}
}
'/' => {
let remaining: String = chars.as_str().to_string();
if modifier_str.starts_with("//") {
let after_double_slash = &remaining[1..]; if let Some(slash_pos) = after_double_slash.find('/') {
let pattern = after_double_slash[..slash_pos].to_string();
let replacement = after_double_slash[slash_pos + 1..].to_string();
Ok(ParameterModifier::SubstituteAll(pattern, replacement))
} else {
Err("Invalid substitution syntax: missing replacement".to_string())
}
} else {
if let Some(slash_pos) = remaining.find('/') {
let pattern = remaining[..slash_pos].to_string();
let replacement = remaining[slash_pos + 1..].to_string();
Ok(ParameterModifier::Substitute(pattern, replacement))
} else {
Err("Invalid substitution syntax: missing replacement".to_string())
}
}
}
'!' => {
let prefix = modifier_str[1..].to_string();
if prefix.ends_with('*') {
Ok(ParameterModifier::IndirectPrefix)
} else if prefix.ends_with('@') {
Ok(ParameterModifier::IndirectPrefixAt)
} else {
Err("Invalid indirect expansion: must end with * or @".to_string())
}
}
_ => Err(format!("Unknown modifier: {}", modifier_str)),
}
}
fn collect_variable_names_with_prefix(prefix: &str, shell_state: &ShellState) -> Vec<String> {
let mut matching_vars = std::collections::HashSet::new();
for var_name in shell_state.variables.keys() {
if var_name.starts_with(prefix) {
matching_vars.insert(var_name.clone());
}
}
for scope in &shell_state.local_vars {
for var_name in scope.keys() {
if var_name.starts_with(prefix) {
matching_vars.insert(var_name.clone());
}
}
}
let mut result: Vec<String> = matching_vars.into_iter().collect();
result.sort();
result
}
pub fn expand_parameter(
expansion: &ParameterExpansion,
shell_state: &ShellState,
) -> Result<String, String> {
let value = match expansion.modifier {
ParameterModifier::None => {
let var_value = shell_state.get_var(&expansion.var_name);
if shell_state.options.nounset && var_value.is_none() {
return Err(format!("{}: unbound variable", expansion.var_name));
}
var_value
}
ParameterModifier::Indirect => {
if let Some(indirect_name) = shell_state.get_var(&expansion.var_name) {
shell_state.get_var(&indirect_name)
} else {
Some("".to_string())
}
}
ParameterModifier::Default(ref default) => {
match shell_state.get_var(&expansion.var_name) {
Some(val) if !val.is_empty() => Some(val),
_ => Some(default.clone()),
}
}
ParameterModifier::AssignDefault(ref default) => {
match shell_state.get_var(&expansion.var_name) {
Some(val) if !val.is_empty() => Some(val),
_ => {
Some(default.clone())
}
}
}
ParameterModifier::Alternative(ref alternative) => {
match shell_state.get_var(&expansion.var_name) {
Some(val) if !val.is_empty() => Some(alternative.clone()),
_ => Some("".to_string()),
}
}
ParameterModifier::Error(ref error_msg) => {
match shell_state.get_var(&expansion.var_name) {
Some(val) if !val.is_empty() => Some(val),
_ => {
let msg = if error_msg.is_empty() {
format!("parameter '{}' not set", expansion.var_name)
} else {
error_msg.clone()
};
return Err(msg);
}
}
}
ParameterModifier::Substring(offset) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
let start = offset.min(val.len());
Some(val[start..].to_string())
} else {
Some("".to_string())
}
}
ParameterModifier::SubstringWithLength(offset, length) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
let start = offset.min(val.len());
let end = (start + length).min(val.len());
Some(val[start..end].to_string())
} else {
Some("".to_string())
}
}
ParameterModifier::RemoveShortestPrefix(ref pattern) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
if let Some(match_end) = find_shortest_prefix_match(pattern, &val) {
Some(val[match_end..].to_string())
} else {
Some(val.clone())
}
} else {
Some("".to_string())
}
}
ParameterModifier::RemoveLongestPrefix(ref pattern) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
if let Some(match_end) = find_longest_prefix_match(pattern, &val) {
Some(val[match_end..].to_string())
} else {
Some(val.clone())
}
} else {
Some("".to_string())
}
}
ParameterModifier::RemoveShortestSuffix(ref pattern) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
if let Some(match_start) = find_shortest_suffix_match(pattern, &val) {
Some(val[..match_start].to_string())
} else {
Some(val.clone())
}
} else {
Some("".to_string())
}
}
ParameterModifier::RemoveLongestSuffix(ref pattern) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
if let Some(match_start) = find_longest_suffix_match(pattern, &val) {
Some(val[..match_start].to_string())
} else {
Some(val.clone())
}
} else {
Some("".to_string())
}
}
ParameterModifier::Substitute(ref pattern, ref replacement) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
Some(val.replace(pattern, replacement))
} else {
Some("".to_string())
}
}
ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
if let Some(val) = shell_state.get_var(&expansion.var_name) {
Some(val.replace(pattern, replacement))
} else {
Some("".to_string())
}
}
ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
let matching_vars =
collect_variable_names_with_prefix(&expansion.var_name, shell_state);
Some(matching_vars.join(" "))
}
};
Ok(value.unwrap_or_else(|| "".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_variable() {
let result = parse_parameter_expansion("VAR").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(result.modifier, ParameterModifier::None);
}
#[test]
fn test_parse_default_modifier() {
let result = parse_parameter_expansion("VAR:-default").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::Default("default".to_string())
);
}
#[test]
fn test_parse_assign_default_modifier() {
let result = parse_parameter_expansion("VAR:=default").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::AssignDefault("default".to_string())
);
}
#[test]
fn test_parse_alternative_modifier() {
let result = parse_parameter_expansion("VAR:+alt").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::Alternative("alt".to_string())
);
}
#[test]
fn test_parse_error_modifier() {
let result = parse_parameter_expansion("VAR:?error").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::Error("error".to_string())
);
}
#[test]
fn test_parse_substring() {
let result = parse_parameter_expansion("VAR:5").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(result.modifier, ParameterModifier::Substring(5));
}
#[test]
fn test_parse_substring_with_length() {
let result = parse_parameter_expansion("VAR:2:3").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::SubstringWithLength(2, 3)
);
}
#[test]
fn test_parse_remove_shortest_prefix() {
let result = parse_parameter_expansion("VAR#prefix").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::RemoveShortestPrefix("prefix".to_string())
);
}
#[test]
fn test_parse_remove_longest_prefix() {
let result = parse_parameter_expansion("VAR##prefix").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::RemoveLongestPrefix("prefix".to_string())
);
}
#[test]
fn test_parse_remove_shortest_suffix() {
let result = parse_parameter_expansion("VAR%suffix").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::RemoveShortestSuffix("suffix".to_string())
);
}
#[test]
fn test_parse_remove_longest_suffix() {
let result = parse_parameter_expansion("VAR%%suffix").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::RemoveLongestSuffix("suffix".to_string())
);
}
#[test]
fn test_parse_substitute() {
let result = parse_parameter_expansion("VAR/old/new").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::Substitute("old".to_string(), "new".to_string())
);
}
#[test]
fn test_parse_substitute_all() {
let result = parse_parameter_expansion("VAR//old/new").unwrap();
assert_eq!(result.var_name, "VAR");
assert_eq!(
result.modifier,
ParameterModifier::SubstituteAll("old".to_string(), "new".to_string())
);
}
#[test]
fn test_parse_indirect_prefix() {
let result = parse_parameter_expansion("!PREFIX*").unwrap();
assert_eq!(result.var_name, "PREFIX");
assert_eq!(result.modifier, ParameterModifier::IndirectPrefix);
}
#[test]
fn test_parse_empty() {
let result = parse_parameter_expansion("");
assert!(result.is_err());
}
#[test]
fn test_parse_invalid_character() {
let result = parse_parameter_expansion("VAR@test");
assert!(result.is_err());
}
#[test]
fn test_expand_simple_variable() {
let mut shell_state = ShellState::new();
shell_state.set_var("TEST_VAR", "hello world".to_string());
let expansion = ParameterExpansion {
var_name: "TEST_VAR".to_string(),
modifier: ParameterModifier::None,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "hello world");
}
#[test]
fn test_expand_default_modifier() {
let mut shell_state = ShellState::new();
shell_state.set_var("TEST_VAR", "value".to_string());
let expansion = ParameterExpansion {
var_name: "TEST_VAR".to_string(),
modifier: ParameterModifier::Default("default".to_string()),
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "value");
}
#[test]
fn test_expand_default_modifier_unset() {
let shell_state = ShellState::new();
let expansion = ParameterExpansion {
var_name: "UNSET_VAR".to_string(),
modifier: ParameterModifier::Default("default".to_string()),
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "default");
}
#[test]
fn test_expand_substring() {
let mut shell_state = ShellState::new();
shell_state.set_var("TEST_VAR", "hello world".to_string());
let expansion = ParameterExpansion {
var_name: "TEST_VAR".to_string(),
modifier: ParameterModifier::Substring(6),
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "world");
}
#[test]
fn test_expand_indirect_prefix_basic() {
let mut shell_state = ShellState::new();
shell_state.set_var("MY_VAR1", "value1".to_string());
shell_state.set_var("MY_VAR2", "value2".to_string());
shell_state.set_var("OTHER_VAR", "other".to_string());
shell_state.set_var("MY_PREFIX_VAR", "prefix".to_string());
let expansion = ParameterExpansion {
var_name: "MY_".to_string(),
modifier: ParameterModifier::IndirectPrefix,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "MY_PREFIX_VAR MY_VAR1 MY_VAR2");
}
#[test]
fn test_expand_indirect_prefix_with_locals() {
let mut shell_state = ShellState::new();
shell_state.set_var("GLOBAL_VAR", "global".to_string());
shell_state.set_var("TEST_VAR1", "test1".to_string());
shell_state.push_local_scope();
shell_state.set_local_var("LOCAL_VAR", "local".to_string());
shell_state.set_local_var("TEST_VAR2", "test2".to_string());
let expansion = ParameterExpansion {
var_name: "TEST_".to_string(),
modifier: ParameterModifier::IndirectPrefix,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "TEST_VAR1 TEST_VAR2");
}
#[test]
fn test_expand_indirect_prefix_no_matches() {
let mut shell_state = ShellState::new();
shell_state.set_var("VAR1", "value1".to_string());
shell_state.set_var("VAR2", "value2".to_string());
let expansion = ParameterExpansion {
var_name: "NONEXISTENT_".to_string(),
modifier: ParameterModifier::IndirectPrefix,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_expand_indirect_prefix_empty_prefix() {
let mut shell_state = ShellState::new();
shell_state.set_var("VAR1", "value1".to_string());
shell_state.set_var("VAR2", "value2".to_string());
shell_state.set_var("ANOTHER_VAR", "another".to_string());
let expansion = ParameterExpansion {
var_name: "".to_string(),
modifier: ParameterModifier::IndirectPrefix,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "ANOTHER_VAR VAR1 VAR2");
}
#[test]
fn test_expand_indirect_prefix_at() {
let mut shell_state = ShellState::new();
shell_state.set_var("PREFIX_VAR1", "value1".to_string());
shell_state.set_var("PREFIX_VAR2", "value2".to_string());
let expansion = ParameterExpansion {
var_name: "PREFIX_".to_string(),
modifier: ParameterModifier::IndirectPrefixAt,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "PREFIX_VAR1 PREFIX_VAR2");
}
#[test]
fn test_expand_indirect_prefix_mixed_scopes() {
let mut shell_state = ShellState::new();
shell_state.set_var("APP_CONFIG", "global_config".to_string());
shell_state.set_var("APP_DEBUG", "false".to_string());
shell_state.push_local_scope();
shell_state.set_local_var("APP_TEMP", "temp_value".to_string());
shell_state.push_local_scope();
shell_state.set_local_var("APP_SECRET", "secret_value".to_string());
let expansion = ParameterExpansion {
var_name: "APP_".to_string(),
modifier: ParameterModifier::IndirectPrefix,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "APP_CONFIG APP_DEBUG APP_SECRET APP_TEMP");
}
#[test]
fn test_expand_indirect_prefix_special_characters() {
let mut shell_state = ShellState::new();
shell_state.set_var("TEST-VAR", "dash".to_string());
shell_state.set_var("TEST.VAR", "dot".to_string());
shell_state.set_var("TEST_VAR", "underscore".to_string());
let expansion = ParameterExpansion {
var_name: "TEST".to_string(),
modifier: ParameterModifier::IndirectPrefix,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "TEST-VAR TEST.VAR TEST_VAR");
}
#[test]
fn test_parse_indirect_basic() {
let result = parse_parameter_expansion("!VAR_NAME").unwrap();
assert_eq!(result.var_name, "VAR_NAME");
assert_eq!(result.modifier, ParameterModifier::Indirect);
}
#[test]
fn test_expand_indirect_basic() {
let mut shell_state = ShellState::new();
shell_state.set_var("VAR_NAME", "TARGET_VAR".to_string());
shell_state.set_var("TARGET_VAR", "final_value".to_string());
let expansion = ParameterExpansion {
var_name: "VAR_NAME".to_string(),
modifier: ParameterModifier::Indirect,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "final_value");
}
#[test]
fn test_expand_indirect_basic_unset_intermediate() {
let mut shell_state = ShellState::new();
shell_state.set_var("TARGET_VAR", "final_value".to_string());
let expansion = ParameterExpansion {
var_name: "VAR_NAME".to_string(),
modifier: ParameterModifier::Indirect,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_expand_indirect_basic_unset_target() {
let mut shell_state = ShellState::new();
shell_state.set_var("VAR_NAME", "NONEXISTENT".to_string());
let expansion = ParameterExpansion {
var_name: "VAR_NAME".to_string(),
modifier: ParameterModifier::Indirect,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_expand_indirect_basic_with_local_scope() {
let mut shell_state = ShellState::new();
shell_state.set_var("VAR_NAME", "GLOBAL_TARGET".to_string());
shell_state.set_var("GLOBAL_TARGET", "global_value".to_string());
shell_state.push_local_scope();
shell_state.set_local_var("VAR_NAME", "LOCAL_TARGET".to_string());
shell_state.set_local_var("LOCAL_TARGET", "local_value".to_string());
let expansion = ParameterExpansion {
var_name: "VAR_NAME".to_string(),
modifier: ParameterModifier::Indirect,
};
let result = expand_parameter(&expansion, &shell_state).unwrap();
assert_eq!(result, "local_value");
}
}