| 1 | use crate::arithmetic::{evaluate_arithmetic, ArithmeticError}; |
| 2 | use crate::brace::expand_brace; |
| 3 | use crate::brace_parse::detect_brace_patterns; |
| 4 | use crate::command_subst::{execute_command_substitution, CommandSubstError}; |
| 5 | use crate::context::Context; |
| 6 | use rush_parser::{VarExpansion, Word, WordPart}; |
| 7 | use thiserror::Error; |
| 8 | |
| 9 | #[derive(Error, Debug)] |
| 10 | pub enum ExpansionError { |
| 11 | #[error("Command substitution failed: {0}")] |
| 12 | CommandSubstitutionFailed(#[from] CommandSubstError), |
| 13 | |
| 14 | #[error("Arithmetic error: {0}")] |
| 15 | ArithmeticError(#[from] ArithmeticError), |
| 16 | |
| 17 | #[error("{0}")] |
| 18 | ParameterError(String), |
| 19 | |
| 20 | #[error("Expansion error: {0}")] |
| 21 | Other(String), |
| 22 | } |
| 23 | |
| 24 | /// Expand a Word into one or more Strings (due to brace expansion) |
| 25 | pub fn expand_word_with_braces(word: &Word, context: &mut Context) -> Result<Vec<String>, ExpansionError> { |
| 26 | // Find if there's a brace expansion |
| 27 | let brace_index = word.parts.iter().position(|p| matches!(p, WordPart::BraceExpansion(_))); |
| 28 | |
| 29 | if let Some(idx) = brace_index { |
| 30 | // Has brace expansion - expand it into multiple words |
| 31 | let mut results = Vec::new(); |
| 32 | |
| 33 | // Get the brace expansion |
| 34 | if let WordPart::BraceExpansion(brace) = &word.parts[idx] { |
| 35 | let expansions = expand_brace(brace); |
| 36 | |
| 37 | // For each expansion, build a complete word |
| 38 | for expanded_part in expansions { |
| 39 | let mut parts_copy = word.parts.clone(); |
| 40 | // Replace the brace expansion with a literal |
| 41 | parts_copy[idx] = WordPart::Literal(expanded_part); |
| 42 | |
| 43 | // Create a new word and expand it (recursively handles nested braces) |
| 44 | let new_word = Word::new(parts_copy); |
| 45 | let mut nested_results = expand_word_with_braces(&new_word, context)?; |
| 46 | results.append(&mut nested_results); |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | Ok(results) |
| 51 | } else { |
| 52 | // No brace expansion - just expand normally |
| 53 | let result = expand_word_simple(word, context)?; |
| 54 | Ok(vec![result]) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /// Expand a Word into a String by resolving all expansions (no brace expansion) |
| 59 | fn expand_word_simple(word: &Word, context: &mut Context) -> Result<String, ExpansionError> { |
| 60 | let mut result = String::new(); |
| 61 | |
| 62 | for part in &word.parts { |
| 63 | match part { |
| 64 | WordPart::Literal(s) => { |
| 65 | // Keep backslash escapes - they'll be stripped during glob expansion |
| 66 | result.push_str(s); |
| 67 | } |
| 68 | WordPart::VarExpansion(var_exp) => { |
| 69 | let expanded = expand_var(var_exp, context)?; |
| 70 | result.push_str(&expanded); |
| 71 | } |
| 72 | WordPart::CommandSubstitution(cmd) => { |
| 73 | let output = execute_command_substitution(cmd, context)?; |
| 74 | result.push_str(&output); |
| 75 | } |
| 76 | WordPart::ArithmeticExpansion(expr) => { |
| 77 | let value = evaluate_arithmetic(expr, context)?; |
| 78 | result.push_str(&value.to_string()); |
| 79 | } |
| 80 | WordPart::BraceExpansion(_) => { |
| 81 | // Should not happen - braces are handled in expand_word_with_braces |
| 82 | return Err(ExpansionError::Other( |
| 83 | "Unexpected brace expansion in simple expansion".to_string(), |
| 84 | )); |
| 85 | } |
| 86 | WordPart::ArrayLiteral(elements) => { |
| 87 | // Array literals like (one two three) expand to space-separated values |
| 88 | let mut values = Vec::new(); |
| 89 | for elem in elements { |
| 90 | values.push(expand_word_simple(elem, context)?); |
| 91 | } |
| 92 | result.push_str(&values.join(" ")); |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | Ok(result) |
| 98 | } |
| 99 | |
| 100 | /// Expand a Word into a String by resolving all expansions |
| 101 | /// (Kept for backward compatibility - delegates to new function) |
| 102 | pub fn expand_word(word: &Word, context: &mut Context) -> Result<String, ExpansionError> { |
| 103 | let results = expand_word_with_braces(word, context)?; |
| 104 | Ok(results.join(" ")) |
| 105 | } |
| 106 | |
| 107 | /// Expand a variable reference |
| 108 | fn expand_var(var_exp: &VarExpansion, context: &mut Context) -> Result<String, ExpansionError> { |
| 109 | match var_exp { |
| 110 | VarExpansion::Simple(name) | VarExpansion::Braced(name) => { |
| 111 | // Handle special variables |
| 112 | match name.as_str() { |
| 113 | "?" => return Ok(context.last_exit_status.to_string()), |
| 114 | "#" => return Ok(context.positional_params.len().to_string()), |
| 115 | "@" | "*" => { |
| 116 | // $@ and $* expand to all positional parameters |
| 117 | // They differ in quoting behavior, but for simple expansion they're the same |
| 118 | return Ok(context.positional_params.join(" ")); |
| 119 | } |
| 120 | "0" => { |
| 121 | // $0 is the shell name or script name |
| 122 | return Ok(context.get_var("0").unwrap_or("rush").to_string()); |
| 123 | } |
| 124 | _ => { |
| 125 | // Check if it's a positional parameter like $1, $2, etc. |
| 126 | if let Ok(index) = name.parse::<usize>() { |
| 127 | if index > 0 && index <= context.positional_params.len() { |
| 128 | return Ok(context.positional_params[index - 1].clone()); |
| 129 | } else { |
| 130 | return Ok(String::new()); |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // Simple expansion: $VAR or ${VAR} |
| 137 | Ok(context.get_var(name).unwrap_or("").to_string()) |
| 138 | } |
| 139 | VarExpansion::WithDefault { name, default } => { |
| 140 | // ${VAR:-default} - use default if VAR is unset or empty |
| 141 | match context.get_var(name) { |
| 142 | Some(value) if !value.is_empty() => Ok(value.to_string()), |
| 143 | _ => { |
| 144 | // Expand the default value recursively |
| 145 | expand_word(default, context) |
| 146 | } |
| 147 | } |
| 148 | } |
| 149 | VarExpansion::AssignDefault { name, default } => { |
| 150 | // ${VAR:=default} - assign default if VAR is unset or empty |
| 151 | match context.get_var(name) { |
| 152 | Some(value) if !value.is_empty() => Ok(value.to_string()), |
| 153 | _ => { |
| 154 | // Expand the default and assign it to the variable |
| 155 | let expanded = expand_word(default, context)?; |
| 156 | let _ = context.set_var(name, &expanded); |
| 157 | Ok(expanded) |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | VarExpansion::UseIfSet { name, alternate } => { |
| 162 | // ${VAR:+alternate} - use alternate if VAR is set and non-empty |
| 163 | match context.get_var(name) { |
| 164 | Some(value) if !value.is_empty() => { |
| 165 | // VAR is set and non-empty, use the alternate |
| 166 | expand_word(alternate, context) |
| 167 | } |
| 168 | _ => { |
| 169 | // VAR is unset or empty, return empty string |
| 170 | Ok(String::new()) |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | VarExpansion::ErrorIfUnset { name, message } => { |
| 175 | // ${VAR:?message} - error if VAR is unset or empty |
| 176 | match context.get_var(name) { |
| 177 | Some(value) if !value.is_empty() => Ok(value.to_string()), |
| 178 | _ => { |
| 179 | // Expand the message for the error |
| 180 | let msg = expand_word(message, context)?; |
| 181 | let err_msg = if msg.is_empty() { |
| 182 | format!("{}: parameter null or not set", name) |
| 183 | } else { |
| 184 | format!("{}: {}", name, msg) |
| 185 | }; |
| 186 | Err(ExpansionError::ParameterError(err_msg)) |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | // Non-colon variants - only check if unset, empty is OK |
| 191 | VarExpansion::WithDefaultUnsetOnly { name, default } => { |
| 192 | // ${VAR-default} - use default only if VAR is unset |
| 193 | match context.get_var(name) { |
| 194 | Some(value) => Ok(value.to_string()), // Empty is fine |
| 195 | None => expand_word(default, context), |
| 196 | } |
| 197 | } |
| 198 | VarExpansion::AssignDefaultUnsetOnly { name, default } => { |
| 199 | // ${VAR=default} - assign default only if VAR is unset |
| 200 | match context.get_var(name) { |
| 201 | Some(value) => Ok(value.to_string()), // Empty is fine |
| 202 | None => { |
| 203 | let expanded = expand_word(default, context)?; |
| 204 | let _ = context.set_var(name, &expanded); |
| 205 | Ok(expanded) |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | VarExpansion::UseIfSetOnly { name, alternate } => { |
| 210 | // ${VAR+alternate} - use alternate if VAR is set (even if empty) |
| 211 | match context.get_var(name) { |
| 212 | Some(_) => expand_word(alternate, context), // Set, even if empty |
| 213 | None => Ok(String::new()), |
| 214 | } |
| 215 | } |
| 216 | VarExpansion::ErrorIfUnsetOnly { name, message } => { |
| 217 | // ${VAR?message} - error only if VAR is unset |
| 218 | match context.get_var(name) { |
| 219 | Some(value) => Ok(value.to_string()), // Empty is fine |
| 220 | None => { |
| 221 | let msg = expand_word(message, context)?; |
| 222 | let err_msg = if msg.is_empty() { |
| 223 | format!("{}: parameter not set", name) |
| 224 | } else { |
| 225 | format!("{}: {}", name, msg) |
| 226 | }; |
| 227 | Err(ExpansionError::ParameterError(err_msg)) |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | VarExpansion::Length(name) => { |
| 232 | // ${#VAR} - return the length of the variable value |
| 233 | let value = context.get_var(name).unwrap_or(""); |
| 234 | Ok(value.len().to_string()) |
| 235 | } |
| 236 | VarExpansion::RemoveShortestPrefix { name, pattern } => { |
| 237 | // ${VAR#pattern} - remove shortest matching prefix |
| 238 | let value = context.get_var(name).unwrap_or("").to_string(); |
| 239 | Ok(remove_prefix(&value, pattern, false)) |
| 240 | } |
| 241 | VarExpansion::RemoveLongestPrefix { name, pattern } => { |
| 242 | // ${VAR##pattern} - remove longest matching prefix |
| 243 | let value = context.get_var(name).unwrap_or("").to_string(); |
| 244 | Ok(remove_prefix(&value, pattern, true)) |
| 245 | } |
| 246 | VarExpansion::RemoveShortestSuffix { name, pattern } => { |
| 247 | // ${VAR%pattern} - remove shortest matching suffix |
| 248 | let value = context.get_var(name).unwrap_or("").to_string(); |
| 249 | Ok(remove_suffix(&value, pattern, false)) |
| 250 | } |
| 251 | VarExpansion::RemoveLongestSuffix { name, pattern } => { |
| 252 | // ${VAR%%pattern} - remove longest matching suffix |
| 253 | let value = context.get_var(name).unwrap_or("").to_string(); |
| 254 | Ok(remove_suffix(&value, pattern, true)) |
| 255 | } |
| 256 | VarExpansion::ReplaceFirst { name, pattern, replacement } => { |
| 257 | // ${VAR/pattern/replacement} - replace first occurrence |
| 258 | let value = context.get_var(name).unwrap_or("").to_string(); |
| 259 | Ok(replace_pattern(&value, pattern, replacement, false)) |
| 260 | } |
| 261 | VarExpansion::ReplaceAll { name, pattern, replacement } => { |
| 262 | // ${VAR//pattern/replacement} - replace all occurrences |
| 263 | let value = context.get_var(name).unwrap_or("").to_string(); |
| 264 | Ok(replace_pattern(&value, pattern, replacement, true)) |
| 265 | } |
| 266 | VarExpansion::Substring { name, offset, length } => { |
| 267 | // ${VAR:offset} or ${VAR:offset:length} |
| 268 | let value = context.get_var(name).unwrap_or(""); |
| 269 | Ok(substring(value, *offset, *length)) |
| 270 | } |
| 271 | VarExpansion::UppercaseFirst(name) => { |
| 272 | // ${VAR^} - uppercase first character |
| 273 | let value = context.get_var(name).unwrap_or(""); |
| 274 | Ok(uppercase_first(value)) |
| 275 | } |
| 276 | VarExpansion::UppercaseAll(name) => { |
| 277 | // ${VAR^^} - uppercase all characters |
| 278 | let value = context.get_var(name).unwrap_or(""); |
| 279 | Ok(value.to_uppercase()) |
| 280 | } |
| 281 | VarExpansion::LowercaseFirst(name) => { |
| 282 | // ${VAR,} - lowercase first character |
| 283 | let value = context.get_var(name).unwrap_or(""); |
| 284 | Ok(lowercase_first(value)) |
| 285 | } |
| 286 | VarExpansion::LowercaseAll(name) => { |
| 287 | // ${VAR,,} - lowercase all characters |
| 288 | let value = context.get_var(name).unwrap_or(""); |
| 289 | Ok(value.to_lowercase()) |
| 290 | } |
| 291 | VarExpansion::ArrayElement { name, index } => { |
| 292 | // ${arr[index]} - get array element at index or key |
| 293 | match context.arrays.get(name) { |
| 294 | Some(crate::context::ArrayType::Indexed(vec)) => { |
| 295 | // Indexed array - parse index as integer |
| 296 | let idx = index.parse::<usize>().unwrap_or(0); |
| 297 | Ok(vec.get(idx).cloned().unwrap_or_default()) |
| 298 | } |
| 299 | Some(crate::context::ArrayType::Associative(map)) => { |
| 300 | // Associative array - use index as string key |
| 301 | Ok(map.get(index).cloned().unwrap_or_default()) |
| 302 | } |
| 303 | None => Ok(String::new()), |
| 304 | } |
| 305 | } |
| 306 | VarExpansion::ArrayAll(name) => { |
| 307 | // ${arr[@]} - all elements as separate words |
| 308 | match context.arrays.get(name) { |
| 309 | Some(crate::context::ArrayType::Indexed(vec)) => Ok(vec.join(" ")), |
| 310 | Some(crate::context::ArrayType::Associative(map)) => { |
| 311 | // For associative arrays, ${arr[@]} returns all values |
| 312 | Ok(map.values().cloned().collect::<Vec<_>>().join(" ")) |
| 313 | } |
| 314 | None => Ok(String::new()), |
| 315 | } |
| 316 | } |
| 317 | VarExpansion::ArrayStar(name) => { |
| 318 | // ${arr[*]} - all elements as single word |
| 319 | match context.arrays.get(name) { |
| 320 | Some(crate::context::ArrayType::Indexed(vec)) => Ok(vec.join(" ")), |
| 321 | Some(crate::context::ArrayType::Associative(map)) => { |
| 322 | // For associative arrays, ${arr[*]} returns all values |
| 323 | Ok(map.values().cloned().collect::<Vec<_>>().join(" ")) |
| 324 | } |
| 325 | None => Ok(String::new()), |
| 326 | } |
| 327 | } |
| 328 | VarExpansion::ArrayLength(name) => { |
| 329 | // ${#arr[@]} - number of elements in array |
| 330 | match context.arrays.get(name) { |
| 331 | Some(crate::context::ArrayType::Indexed(vec)) => Ok(vec.len().to_string()), |
| 332 | Some(crate::context::ArrayType::Associative(map)) => Ok(map.len().to_string()), |
| 333 | None => Ok("0".to_string()), |
| 334 | } |
| 335 | } |
| 336 | VarExpansion::ArrayIndices(name) => { |
| 337 | // ${!arr[@]} - array indices or keys |
| 338 | match context.arrays.get(name) { |
| 339 | Some(crate::context::ArrayType::Indexed(vec)) => { |
| 340 | // For indexed arrays, return numeric indices |
| 341 | let indices: Vec<String> = (0..vec.len()).map(|i| i.to_string()).collect(); |
| 342 | Ok(indices.join(" ")) |
| 343 | } |
| 344 | Some(crate::context::ArrayType::Associative(map)) => { |
| 345 | // For associative arrays, return keys |
| 346 | Ok(map.keys().cloned().collect::<Vec<_>>().join(" ")) |
| 347 | } |
| 348 | None => Ok(String::new()), |
| 349 | } |
| 350 | } |
| 351 | VarExpansion::Indirect(name) => { |
| 352 | // ${!var} - indirect expansion |
| 353 | // First get the value of the variable, then use that as a variable name |
| 354 | let indirect_name = context.get_var(name).unwrap_or(""); |
| 355 | if indirect_name.is_empty() { |
| 356 | Ok(String::new()) |
| 357 | } else { |
| 358 | Ok(context.get_var(indirect_name).unwrap_or("").to_string()) |
| 359 | } |
| 360 | } |
| 361 | VarExpansion::Transform { name, op } => { |
| 362 | // ${var@op} - transformation operators |
| 363 | let value = context.get_var(name).unwrap_or(""); |
| 364 | Ok(apply_transform(value, *op)) |
| 365 | } |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | /// Apply transformation operator to a value |
| 370 | fn apply_transform(value: &str, op: char) -> String { |
| 371 | match op { |
| 372 | 'Q' => { |
| 373 | // Quote for reuse as input (single-quoted format) |
| 374 | format!("'{}'", value.replace('\'', "'\\''")) |
| 375 | } |
| 376 | 'E' => { |
| 377 | // Expand escape sequences like $'...' |
| 378 | expand_escapes(value) |
| 379 | } |
| 380 | 'P' => { |
| 381 | // Expand as prompt string (simplified - just return value) |
| 382 | value.to_string() |
| 383 | } |
| 384 | 'A' => { |
| 385 | // Assignment statement format (simplified) |
| 386 | value.to_string() |
| 387 | } |
| 388 | 'K' => { |
| 389 | // Quote for reuse, associative array format |
| 390 | format!("'{}'", value.replace('\'', "'\\''")) |
| 391 | } |
| 392 | 'a' => { |
| 393 | // Attributes (simplified - return empty) |
| 394 | String::new() |
| 395 | } |
| 396 | 'u' | 'U' => { |
| 397 | // Uppercase (U = all, u = first) |
| 398 | if op == 'U' { |
| 399 | value.to_uppercase() |
| 400 | } else { |
| 401 | uppercase_first(value) |
| 402 | } |
| 403 | } |
| 404 | 'L' => { |
| 405 | // Lowercase all |
| 406 | value.to_lowercase() |
| 407 | } |
| 408 | _ => value.to_string(), |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | /// Expand escape sequences like \n, \t, etc. |
| 413 | fn expand_escapes(s: &str) -> String { |
| 414 | let mut result = String::new(); |
| 415 | let mut chars = s.chars().peekable(); |
| 416 | |
| 417 | while let Some(ch) = chars.next() { |
| 418 | if ch == '\\' { |
| 419 | if let Some(&next) = chars.peek() { |
| 420 | chars.next(); |
| 421 | match next { |
| 422 | 'n' => result.push('\n'), |
| 423 | 't' => result.push('\t'), |
| 424 | 'r' => result.push('\r'), |
| 425 | '\\' => result.push('\\'), |
| 426 | '\'' => result.push('\''), |
| 427 | '"' => result.push('"'), |
| 428 | '0' => result.push('\0'), |
| 429 | 'a' => result.push('\x07'), // Bell |
| 430 | 'b' => result.push('\x08'), // Backspace |
| 431 | 'e' | 'E' => result.push('\x1b'), // Escape |
| 432 | 'f' => result.push('\x0c'), // Form feed |
| 433 | 'v' => result.push('\x0b'), // Vertical tab |
| 434 | _ => { |
| 435 | result.push('\\'); |
| 436 | result.push(next); |
| 437 | } |
| 438 | } |
| 439 | } else { |
| 440 | result.push('\\'); |
| 441 | } |
| 442 | } else { |
| 443 | result.push(ch); |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | result |
| 448 | } |
| 449 | |
| 450 | /// Remove prefix from string based on pattern |
| 451 | /// If greedy is true, removes the longest match; otherwise shortest |
| 452 | fn remove_prefix(value: &str, pattern: &str, greedy: bool) -> String { |
| 453 | // Simple glob pattern matching with * wildcard |
| 454 | if pattern.contains('*') { |
| 455 | // For patterns like "a*b", we match from start |
| 456 | let parts: Vec<&str> = pattern.split('*').collect(); |
| 457 | if parts.len() == 2 { |
| 458 | let prefix = parts[0]; |
| 459 | let suffix = parts[1]; |
| 460 | |
| 461 | if value.starts_with(prefix) { |
| 462 | if greedy { |
| 463 | // Find the last occurrence of suffix |
| 464 | if let Some(pos) = value.rfind(suffix) { |
| 465 | return value[pos + suffix.len()..].to_string(); |
| 466 | } |
| 467 | } else { |
| 468 | // Find the first occurrence of suffix after prefix |
| 469 | if let Some(pos) = value[prefix.len()..].find(suffix) { |
| 470 | return value[prefix.len() + pos + suffix.len()..].to_string(); |
| 471 | } |
| 472 | } |
| 473 | } |
| 474 | } |
| 475 | } else { |
| 476 | // Literal pattern - just remove if it's a prefix |
| 477 | if value.starts_with(pattern) { |
| 478 | return value[pattern.len()..].to_string(); |
| 479 | } |
| 480 | } |
| 481 | value.to_string() |
| 482 | } |
| 483 | |
| 484 | /// Remove suffix from string based on pattern |
| 485 | /// If greedy is true, removes the longest match; otherwise shortest |
| 486 | fn remove_suffix(value: &str, pattern: &str, greedy: bool) -> String { |
| 487 | // Simple glob pattern matching with * wildcard |
| 488 | if pattern.contains('*') { |
| 489 | let parts: Vec<&str> = pattern.split('*').collect(); |
| 490 | if parts.len() == 2 { |
| 491 | let prefix = parts[0]; |
| 492 | let suffix = parts[1]; |
| 493 | |
| 494 | if value.ends_with(suffix) { |
| 495 | if greedy { |
| 496 | // Find the first occurrence of prefix |
| 497 | if let Some(pos) = value.find(prefix) { |
| 498 | return value[..pos].to_string(); |
| 499 | } |
| 500 | } else { |
| 501 | // Find the last occurrence of prefix before suffix |
| 502 | let end_without_suffix = value.len() - suffix.len(); |
| 503 | if let Some(pos) = value[..end_without_suffix].rfind(prefix) { |
| 504 | return value[..pos].to_string(); |
| 505 | } |
| 506 | } |
| 507 | } |
| 508 | } |
| 509 | } else { |
| 510 | // Literal pattern - just remove if it's a suffix |
| 511 | if value.ends_with(pattern) { |
| 512 | return value[..value.len() - pattern.len()].to_string(); |
| 513 | } |
| 514 | } |
| 515 | value.to_string() |
| 516 | } |
| 517 | |
| 518 | /// Replace pattern in string |
| 519 | /// If all is true, replaces all occurrences; otherwise just first |
| 520 | fn replace_pattern(value: &str, pattern: &str, replacement: &str, all: bool) -> String { |
| 521 | // For now, treat pattern as literal (can be extended to glob patterns) |
| 522 | if all { |
| 523 | value.replace(pattern, replacement) |
| 524 | } else { |
| 525 | value.replacen(pattern, replacement, 1) |
| 526 | } |
| 527 | } |
| 528 | |
| 529 | /// Extract substring from value |
| 530 | /// Offset can be negative (from end), length is optional |
| 531 | fn substring(value: &str, offset: i32, length: Option<usize>) -> String { |
| 532 | let len = value.len() as i32; |
| 533 | |
| 534 | // Calculate actual start position |
| 535 | let start = if offset < 0 { |
| 536 | // Negative offset: count from end |
| 537 | let pos = len + offset; |
| 538 | if pos < 0 { |
| 539 | 0 |
| 540 | } else { |
| 541 | pos as usize |
| 542 | } |
| 543 | } else { |
| 544 | // Positive offset: from start |
| 545 | if offset as usize > value.len() { |
| 546 | return String::new(); |
| 547 | } |
| 548 | offset as usize |
| 549 | }; |
| 550 | |
| 551 | // Calculate end position |
| 552 | match length { |
| 553 | Some(len) => { |
| 554 | let end = (start + len).min(value.len()); |
| 555 | value[start..end].to_string() |
| 556 | } |
| 557 | None => { |
| 558 | value[start..].to_string() |
| 559 | } |
| 560 | } |
| 561 | } |
| 562 | |
| 563 | /// Uppercase first character |
| 564 | fn uppercase_first(value: &str) -> String { |
| 565 | let mut chars = value.chars(); |
| 566 | match chars.next() { |
| 567 | Some(first) => { |
| 568 | let mut result = first.to_uppercase().to_string(); |
| 569 | result.push_str(chars.as_str()); |
| 570 | result |
| 571 | } |
| 572 | None => String::new(), |
| 573 | } |
| 574 | } |
| 575 | |
| 576 | /// Lowercase first character |
| 577 | fn lowercase_first(value: &str) -> String { |
| 578 | let mut chars = value.chars(); |
| 579 | match chars.next() { |
| 580 | Some(first) => { |
| 581 | let mut result = first.to_lowercase().to_string(); |
| 582 | result.push_str(chars.as_str()); |
| 583 | result |
| 584 | } |
| 585 | None => String::new(), |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | /// Expand multiple words (e.g., command arguments) |
| 590 | /// Each word may expand into multiple words due to brace expansion and glob expansion |
| 591 | pub fn expand_words(words: &[Word], context: &mut Context) -> Result<Vec<String>, ExpansionError> { |
| 592 | let mut results = Vec::new(); |
| 593 | for word in words { |
| 594 | // First, detect and parse any brace patterns in literals |
| 595 | let word_with_braces = detect_brace_patterns(word); |
| 596 | |
| 597 | // Then expand the word (which may produce multiple results due to braces) |
| 598 | let expanded = expand_word_with_braces(&word_with_braces, context)?; |
| 599 | |
| 600 | // Finally, apply glob expansion to each expanded word |
| 601 | for expanded_word in expanded { |
| 602 | let glob_options = crate::glob::GlobOptions { |
| 603 | match_dotfiles: context.options.dotglob, |
| 604 | globstar: true, |
| 605 | nullglob: context.options.nullglob, |
| 606 | extglob: context.options.extglob, |
| 607 | }; |
| 608 | match crate::glob::expand_glob(&expanded_word, &glob_options) { |
| 609 | Ok(mut glob_results) => { |
| 610 | results.append(&mut glob_results); |
| 611 | } |
| 612 | Err(_) => { |
| 613 | // If glob expansion fails, use the literal word |
| 614 | results.push(expanded_word); |
| 615 | } |
| 616 | } |
| 617 | } |
| 618 | } |
| 619 | Ok(results) |
| 620 | } |
| 621 | |
| 622 | #[cfg(test)] |
| 623 | mod tests { |
| 624 | use super::*; |
| 625 | use rush_parser::Word; |
| 626 | |
| 627 | #[test] |
| 628 | fn test_expand_literal() { |
| 629 | let mut ctx = Context::empty(); |
| 630 | let word = Word::from_literal("hello"); |
| 631 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 632 | assert_eq!(result, "hello"); |
| 633 | } |
| 634 | |
| 635 | #[test] |
| 636 | fn test_expand_simple_var() { |
| 637 | let mut ctx = Context::empty(); |
| 638 | ctx.set_var("USER", "alice").unwrap(); |
| 639 | |
| 640 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Simple( |
| 641 | "USER".to_string(), |
| 642 | ))]); |
| 643 | |
| 644 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 645 | assert_eq!(result, "alice"); |
| 646 | } |
| 647 | |
| 648 | #[test] |
| 649 | fn test_expand_undefined_var() { |
| 650 | let mut ctx = Context::empty(); |
| 651 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Simple( |
| 652 | "UNDEFINED".to_string(), |
| 653 | ))]); |
| 654 | |
| 655 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 656 | assert_eq!(result, ""); |
| 657 | } |
| 658 | |
| 659 | #[test] |
| 660 | fn test_expand_var_with_default() { |
| 661 | let mut ctx = Context::empty(); |
| 662 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::WithDefault { |
| 663 | name: "UNDEFINED".to_string(), |
| 664 | default: Box::new(Word::from_literal("default_value")), |
| 665 | })]); |
| 666 | |
| 667 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 668 | assert_eq!(result, "default_value"); |
| 669 | } |
| 670 | |
| 671 | #[test] |
| 672 | fn test_expand_mixed_word() { |
| 673 | let mut ctx = Context::empty(); |
| 674 | ctx.set_var("NAME", "world").unwrap(); |
| 675 | |
| 676 | let word = Word::new(vec![ |
| 677 | WordPart::Literal("Hello ".to_string()), |
| 678 | WordPart::VarExpansion(VarExpansion::Simple("NAME".to_string())), |
| 679 | WordPart::Literal("!".to_string()), |
| 680 | ]); |
| 681 | |
| 682 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 683 | assert_eq!(result, "Hello world!"); |
| 684 | } |
| 685 | |
| 686 | #[test] |
| 687 | fn test_expand_multiple_words() { |
| 688 | let mut ctx = Context::empty(); |
| 689 | ctx.set_var("CMD", "ls").unwrap(); |
| 690 | |
| 691 | let words = vec![ |
| 692 | Word::new(vec![WordPart::VarExpansion(VarExpansion::Simple( |
| 693 | "CMD".to_string(), |
| 694 | ))]), |
| 695 | Word::from_literal("-la"), |
| 696 | ]; |
| 697 | |
| 698 | let result = expand_words(&words, &mut ctx).unwrap(); |
| 699 | assert_eq!(result, vec!["ls", "-la"]); |
| 700 | } |
| 701 | |
| 702 | #[test] |
| 703 | fn test_expand_command_substitution() { |
| 704 | let mut ctx = Context::empty(); |
| 705 | let word = Word::new(vec![WordPart::CommandSubstitution("echo hello".to_string())]); |
| 706 | |
| 707 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 708 | assert_eq!(result, "hello"); |
| 709 | } |
| 710 | |
| 711 | #[test] |
| 712 | fn test_expand_mixed_with_command_subst() { |
| 713 | let mut ctx = Context::empty(); |
| 714 | let word = Word::new(vec![ |
| 715 | WordPart::Literal("Result: ".to_string()), |
| 716 | WordPart::CommandSubstitution("echo success".to_string()), |
| 717 | ]); |
| 718 | |
| 719 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 720 | assert_eq!(result, "Result: success"); |
| 721 | } |
| 722 | |
| 723 | #[test] |
| 724 | fn test_indirect_expansion() { |
| 725 | let mut ctx = Context::empty(); |
| 726 | ctx.set_var("ptr", "target").unwrap(); |
| 727 | ctx.set_var("target", "hello world").unwrap(); |
| 728 | |
| 729 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Indirect( |
| 730 | "ptr".to_string(), |
| 731 | ))]); |
| 732 | |
| 733 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 734 | assert_eq!(result, "hello world"); |
| 735 | } |
| 736 | |
| 737 | #[test] |
| 738 | fn test_indirect_expansion_missing() { |
| 739 | let mut ctx = Context::empty(); |
| 740 | ctx.set_var("ptr", "nonexistent").unwrap(); |
| 741 | |
| 742 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Indirect( |
| 743 | "ptr".to_string(), |
| 744 | ))]); |
| 745 | |
| 746 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 747 | assert_eq!(result, ""); |
| 748 | } |
| 749 | |
| 750 | #[test] |
| 751 | fn test_transform_quote() { |
| 752 | let mut ctx = Context::empty(); |
| 753 | ctx.set_var("msg", "hello world").unwrap(); |
| 754 | |
| 755 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Transform { |
| 756 | name: "msg".to_string(), |
| 757 | op: 'Q', |
| 758 | })]); |
| 759 | |
| 760 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 761 | assert_eq!(result, "'hello world'"); |
| 762 | } |
| 763 | |
| 764 | #[test] |
| 765 | fn test_transform_quote_with_apostrophe() { |
| 766 | let mut ctx = Context::empty(); |
| 767 | ctx.set_var("msg", "don't stop").unwrap(); |
| 768 | |
| 769 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Transform { |
| 770 | name: "msg".to_string(), |
| 771 | op: 'Q', |
| 772 | })]); |
| 773 | |
| 774 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 775 | assert_eq!(result, "'don'\\''t stop'"); |
| 776 | } |
| 777 | |
| 778 | #[test] |
| 779 | fn test_transform_uppercase() { |
| 780 | let mut ctx = Context::empty(); |
| 781 | ctx.set_var("name", "hello").unwrap(); |
| 782 | |
| 783 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Transform { |
| 784 | name: "name".to_string(), |
| 785 | op: 'U', |
| 786 | })]); |
| 787 | |
| 788 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 789 | assert_eq!(result, "HELLO"); |
| 790 | } |
| 791 | |
| 792 | #[test] |
| 793 | fn test_transform_lowercase() { |
| 794 | let mut ctx = Context::empty(); |
| 795 | ctx.set_var("name", "HELLO").unwrap(); |
| 796 | |
| 797 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Transform { |
| 798 | name: "name".to_string(), |
| 799 | op: 'L', |
| 800 | })]); |
| 801 | |
| 802 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 803 | assert_eq!(result, "hello"); |
| 804 | } |
| 805 | |
| 806 | #[test] |
| 807 | fn test_transform_escape() { |
| 808 | let mut ctx = Context::empty(); |
| 809 | ctx.set_var("text", "hello\\nworld").unwrap(); |
| 810 | |
| 811 | let word = Word::new(vec![WordPart::VarExpansion(VarExpansion::Transform { |
| 812 | name: "text".to_string(), |
| 813 | op: 'E', |
| 814 | })]); |
| 815 | |
| 816 | let result = expand_word(&word, &mut ctx).unwrap(); |
| 817 | assert_eq!(result, "hello\nworld"); |
| 818 | } |
| 819 | } |
| 820 |