Rust · 30351 bytes Raw Blame History
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