1use super::state::ShellState;
3
4fn glob_match(pattern: &str, text: &str) -> bool {
7 glob_match_recursive(pattern, text, 0, 0)
8}
9
10fn glob_match_recursive(pattern: &str, text: &str, pi: usize, ti: usize) -> bool {
11 if pi >= pattern.len() {
13 return ti >= text.len();
14 }
15
16 if ti >= text.len() {
18 return pattern[pi..].chars().all(|c| c == '*');
19 }
20
21 match pattern.chars().nth(pi).unwrap() {
22 '*' => {
23 if glob_match_recursive(pattern, text, pi + 1, ti) {
26 return true;
27 }
28 if ti < text.len() {
30 return glob_match_recursive(pattern, text, pi, ti + 1);
31 }
32 false
33 }
34 c => {
35 if c == text.chars().nth(ti).unwrap() {
37 glob_match_recursive(pattern, text, pi + 1, ti + 1)
38 } else {
39 false
40 }
41 }
42 }
43}
44
45fn find_shortest_prefix_match(pattern: &str, text: &str) -> Option<usize> {
47 if pattern.is_empty() {
48 return Some(0);
49 }
50
51 for i in 0..=text.len() {
52 let prefix = &text[..i];
53 if glob_match(pattern, prefix) {
54 return Some(i);
55 }
56 }
57 None
58}
59
60fn find_longest_prefix_match(pattern: &str, text: &str) -> Option<usize> {
62 if pattern.is_empty() {
63 return Some(0);
64 }
65
66 let mut longest = None;
67 for i in 0..=text.len() {
68 let prefix = &text[..i];
69 if glob_match(pattern, prefix) {
70 longest = Some(i);
71 }
72 }
73 longest
74}
75
76fn find_shortest_suffix_match(pattern: &str, text: &str) -> Option<usize> {
78 if pattern.is_empty() {
79 return Some(text.len());
80 }
81
82 for i in (0..=text.len()).rev() {
83 let suffix = &text[i..];
84 if glob_match(pattern, suffix) {
85 return Some(i);
86 }
87 }
88 None
89}
90
91fn find_longest_suffix_match(pattern: &str, text: &str) -> Option<usize> {
93 if pattern.is_empty() {
94 return Some(text.len());
95 }
96
97 let mut longest = None;
98 for i in (0..=text.len()).rev() {
99 let suffix = &text[i..];
100 if glob_match(pattern, suffix) {
101 longest = Some(i);
102 }
103 }
104 longest
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum ParameterModifier {
110 None,
112 Default(String),
114 AssignDefault(String),
116 Alternative(String),
118 Error(String),
120 Substring(usize),
122 SubstringWithLength(usize, usize),
124 RemoveShortestPrefix(String),
126 RemoveLongestPrefix(String),
128 RemoveShortestSuffix(String),
130 RemoveLongestSuffix(String),
132 Substitute(String, String),
134 SubstituteAll(String, String),
136 IndirectPrefix,
138 IndirectPrefixAt,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct ParameterExpansion {
145 pub var_name: String,
146 pub modifier: ParameterModifier,
147}
148
149pub fn parse_parameter_expansion(content: &str) -> Result<ParameterExpansion, String> {
151 if content.is_empty() {
152 return Err("Empty parameter expansion".to_string());
153 }
154
155 let chars = content.chars();
156 let mut var_name = String::new();
157
158 for ch in chars {
160 if ch == ':' || ch == '#' || ch == '%' || ch == '/' {
161 let modifier_str: String = content[var_name.len()..].to_string();
163 let modifier = parse_modifier(&modifier_str)?;
164 return Ok(ParameterExpansion { var_name, modifier });
165 } else if ch == '!' {
166 var_name.push(ch);
169 } else if ch.is_alphanumeric() || ch == '_' || ch == '*' {
170 var_name.push(ch);
172 } else {
173 return Err(format!("Invalid character '{}' in variable name", ch));
174 }
175 }
176
177 let (final_var_name, modifier) = if var_name.starts_with('!') {
179 if var_name.ends_with('*') {
180 (
182 var_name[..var_name.len() - 1].to_string(),
183 ParameterModifier::IndirectPrefix,
184 )
185 } else if var_name.ends_with('@') {
186 (
188 var_name[..var_name.len() - 1].to_string(),
189 ParameterModifier::IndirectPrefixAt,
190 )
191 } else {
192 return Err("Invalid indirect expansion: must end with * or @".to_string());
193 }
194 } else {
195 (var_name, ParameterModifier::None)
196 };
197
198 Ok(ParameterExpansion {
199 var_name: final_var_name,
200 modifier,
201 })
202}
203
204fn parse_modifier(modifier_str: &str) -> Result<ParameterModifier, String> {
206 if modifier_str.is_empty() {
207 return Ok(ParameterModifier::None);
208 }
209
210 let mut chars = modifier_str.chars();
211
212 match chars.next().unwrap() {
213 ':' => {
214 match chars.next() {
215 Some('=') => {
216 let word = modifier_str[2..].to_string();
218 Ok(ParameterModifier::AssignDefault(word))
219 }
220 Some('-') => {
221 let word = modifier_str[2..].to_string();
223 Ok(ParameterModifier::Default(word))
224 }
225 Some('+') => {
226 let word = modifier_str[2..].to_string();
228 Ok(ParameterModifier::Alternative(word))
229 }
230 Some('?') => {
231 let word = modifier_str[2..].to_string();
233 Ok(ParameterModifier::Error(word))
234 }
235 Some(ch) if ch.is_ascii_digit() => {
236 let after_colon = &modifier_str[1..]; let offset_end = after_colon.find(':').unwrap_or(after_colon.len());
242 let offset_str = &after_colon[..offset_end];
243
244 if offset_str.is_empty() {
245 return Err("Missing offset in substring operation".to_string());
246 }
247
248 let offset: usize = offset_str.parse().map_err(|_| "Invalid offset number")?;
249
250 if offset_end < after_colon.len() {
252 let after_offset = &after_colon[offset_end + 1..]; if !after_offset.is_empty()
255 && after_offset.chars().all(|c| c.is_ascii_digit())
256 {
257 let length: usize =
258 after_offset.parse().map_err(|_| "Invalid length number")?;
259 Ok(ParameterModifier::SubstringWithLength(offset, length))
260 } else {
261 Ok(ParameterModifier::Substring(offset))
262 }
263 } else {
264 Ok(ParameterModifier::Substring(offset))
265 }
266 }
267 _ => Err(format!("Invalid modifier: {}", modifier_str)),
268 }
269 }
270 '#' => {
271 if let Some(pattern) = modifier_str.strip_prefix("##") {
272 Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
274 } else if let Some(pattern) = modifier_str.strip_prefix('#') {
275 Ok(ParameterModifier::RemoveShortestPrefix(pattern.to_string()))
277 } else {
278 Err(format!("Invalid prefix removal modifier: {}", modifier_str))
279 }
280 }
281 '%' => {
282 if let Some(pattern) = modifier_str.strip_prefix("%%") {
283 Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
285 } else if let Some(pattern) = modifier_str.strip_prefix('%') {
286 Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
288 } else {
289 Err(format!("Invalid suffix removal modifier: {}", modifier_str))
290 }
291 }
292 '/' => {
293 let remaining: String = chars.as_str().to_string();
295
296 if modifier_str.starts_with("//") {
297 let after_double_slash = &remaining[1..]; if let Some(slash_pos) = after_double_slash.find('/') {
300 let pattern = after_double_slash[..slash_pos].to_string();
301 let replacement = after_double_slash[slash_pos + 1..].to_string();
302 Ok(ParameterModifier::SubstituteAll(pattern, replacement))
303 } else {
304 Err("Invalid substitution syntax: missing replacement".to_string())
305 }
306 } else {
307 if let Some(slash_pos) = remaining.find('/') {
309 let pattern = remaining[..slash_pos].to_string();
310 let replacement = remaining[slash_pos + 1..].to_string();
311 Ok(ParameterModifier::Substitute(pattern, replacement))
312 } else {
313 Err("Invalid substitution syntax: missing replacement".to_string())
314 }
315 }
316 }
317 '!' => {
318 let prefix = modifier_str[1..].to_string();
319 if prefix.ends_with('*') {
320 Ok(ParameterModifier::IndirectPrefix)
321 } else if prefix.ends_with('@') {
322 Ok(ParameterModifier::IndirectPrefixAt)
323 } else {
324 Err("Invalid indirect expansion: must end with * or @".to_string())
325 }
326 }
327 _ => Err(format!("Unknown modifier: {}", modifier_str)),
328 }
329}
330
331pub fn expand_parameter(
333 expansion: &ParameterExpansion,
334 shell_state: &ShellState,
335) -> Result<String, String> {
336 let value = match expansion.modifier {
337 ParameterModifier::None => {
338 shell_state.get_var(&expansion.var_name)
340 }
341 ParameterModifier::Default(ref default) => {
342 match shell_state.get_var(&expansion.var_name) {
344 Some(val) if !val.is_empty() => Some(val),
345 _ => Some(default.clone()),
346 }
347 }
348 ParameterModifier::AssignDefault(ref default) => {
349 match shell_state.get_var(&expansion.var_name) {
351 Some(val) if !val.is_empty() => Some(val),
352 _ => {
353 Some(default.clone())
355 }
356 }
357 }
358 ParameterModifier::Alternative(ref alternative) => {
359 match shell_state.get_var(&expansion.var_name) {
361 Some(val) if !val.is_empty() => Some(alternative.clone()),
362 _ => Some("".to_string()),
363 }
364 }
365 ParameterModifier::Error(ref error_msg) => {
366 match shell_state.get_var(&expansion.var_name) {
368 Some(val) if !val.is_empty() => Some(val),
369 _ => {
370 let msg = if error_msg.is_empty() {
371 format!("parameter '{}' not set", expansion.var_name)
372 } else {
373 error_msg.clone()
374 };
375 return Err(msg);
376 }
377 }
378 }
379 ParameterModifier::Substring(offset) => {
380 if let Some(val) = shell_state.get_var(&expansion.var_name) {
382 let start = offset.min(val.len());
383 Some(val[start..].to_string())
384 } else {
385 Some("".to_string())
386 }
387 }
388 ParameterModifier::SubstringWithLength(offset, length) => {
389 if let Some(val) = shell_state.get_var(&expansion.var_name) {
391 let start = offset.min(val.len());
392 let end = (start + length).min(val.len());
393 Some(val[start..end].to_string())
394 } else {
395 Some("".to_string())
396 }
397 }
398 ParameterModifier::RemoveShortestPrefix(ref pattern) => {
399 if let Some(val) = shell_state.get_var(&expansion.var_name) {
401 if let Some(match_end) = find_shortest_prefix_match(pattern, &val) {
402 Some(val[match_end..].to_string())
403 } else {
404 Some(val.clone())
405 }
406 } else {
407 Some("".to_string())
408 }
409 }
410 ParameterModifier::RemoveLongestPrefix(ref pattern) => {
411 if let Some(val) = shell_state.get_var(&expansion.var_name) {
413 if let Some(match_end) = find_longest_prefix_match(pattern, &val) {
414 Some(val[match_end..].to_string())
415 } else {
416 Some(val.clone())
417 }
418 } else {
419 Some("".to_string())
420 }
421 }
422 ParameterModifier::RemoveShortestSuffix(ref pattern) => {
423 if let Some(val) = shell_state.get_var(&expansion.var_name) {
425 if let Some(match_start) = find_shortest_suffix_match(pattern, &val) {
426 Some(val[..match_start].to_string())
427 } else {
428 Some(val.clone())
429 }
430 } else {
431 Some("".to_string())
432 }
433 }
434 ParameterModifier::RemoveLongestSuffix(ref pattern) => {
435 if let Some(val) = shell_state.get_var(&expansion.var_name) {
437 if let Some(match_start) = find_longest_suffix_match(pattern, &val) {
438 Some(val[..match_start].to_string())
439 } else {
440 Some(val.clone())
441 }
442 } else {
443 Some("".to_string())
444 }
445 }
446 ParameterModifier::Substitute(ref pattern, ref replacement) => {
447 if let Some(val) = shell_state.get_var(&expansion.var_name) {
449 Some(val.replace(pattern, replacement))
451 } else {
452 Some("".to_string())
453 }
454 }
455 ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
456 if let Some(val) = shell_state.get_var(&expansion.var_name) {
458 Some(val.replace(pattern, replacement))
460 } else {
461 Some("".to_string())
462 }
463 }
464 ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
465 Some("".to_string())
469 }
470 };
471
472 Ok(value.unwrap_or_else(|| "".to_string()))
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_parse_simple_variable() {
481 let result = parse_parameter_expansion("VAR").unwrap();
482 assert_eq!(result.var_name, "VAR");
483 assert_eq!(result.modifier, ParameterModifier::None);
484 }
485
486 #[test]
487 fn test_parse_default_modifier() {
488 let result = parse_parameter_expansion("VAR:-default").unwrap();
489 assert_eq!(result.var_name, "VAR");
490 assert_eq!(
491 result.modifier,
492 ParameterModifier::Default("default".to_string())
493 );
494 }
495
496 #[test]
497 fn test_parse_assign_default_modifier() {
498 let result = parse_parameter_expansion("VAR:=default").unwrap();
499 assert_eq!(result.var_name, "VAR");
500 assert_eq!(
501 result.modifier,
502 ParameterModifier::AssignDefault("default".to_string())
503 );
504 }
505
506 #[test]
507 fn test_parse_alternative_modifier() {
508 let result = parse_parameter_expansion("VAR:+alt").unwrap();
509 assert_eq!(result.var_name, "VAR");
510 assert_eq!(
511 result.modifier,
512 ParameterModifier::Alternative("alt".to_string())
513 );
514 }
515
516 #[test]
517 fn test_parse_error_modifier() {
518 let result = parse_parameter_expansion("VAR:?error").unwrap();
519 assert_eq!(result.var_name, "VAR");
520 assert_eq!(
521 result.modifier,
522 ParameterModifier::Error("error".to_string())
523 );
524 }
525
526 #[test]
527 fn test_parse_substring() {
528 let result = parse_parameter_expansion("VAR:5").unwrap();
529 assert_eq!(result.var_name, "VAR");
530 assert_eq!(result.modifier, ParameterModifier::Substring(5));
531 }
532
533 #[test]
534 fn test_parse_substring_with_length() {
535 let result = parse_parameter_expansion("VAR:2:3").unwrap();
536 assert_eq!(result.var_name, "VAR");
537 assert_eq!(
538 result.modifier,
539 ParameterModifier::SubstringWithLength(2, 3)
540 );
541 }
542
543 #[test]
544 fn test_parse_remove_shortest_prefix() {
545 let result = parse_parameter_expansion("VAR#prefix").unwrap();
546 assert_eq!(result.var_name, "VAR");
547 assert_eq!(
548 result.modifier,
549 ParameterModifier::RemoveShortestPrefix("prefix".to_string())
550 );
551 }
552
553 #[test]
554 fn test_parse_remove_longest_prefix() {
555 let result = parse_parameter_expansion("VAR##prefix").unwrap();
556 assert_eq!(result.var_name, "VAR");
557 assert_eq!(
558 result.modifier,
559 ParameterModifier::RemoveLongestPrefix("prefix".to_string())
560 );
561 }
562
563 #[test]
564 fn test_parse_remove_shortest_suffix() {
565 let result = parse_parameter_expansion("VAR%suffix").unwrap();
566 assert_eq!(result.var_name, "VAR");
567 assert_eq!(
568 result.modifier,
569 ParameterModifier::RemoveShortestSuffix("suffix".to_string())
570 );
571 }
572
573 #[test]
574 fn test_parse_remove_longest_suffix() {
575 let result = parse_parameter_expansion("VAR%%suffix").unwrap();
576 assert_eq!(result.var_name, "VAR");
577 assert_eq!(
578 result.modifier,
579 ParameterModifier::RemoveLongestSuffix("suffix".to_string())
580 );
581 }
582
583 #[test]
584 fn test_parse_substitute() {
585 let result = parse_parameter_expansion("VAR/old/new").unwrap();
586 assert_eq!(result.var_name, "VAR");
587 assert_eq!(
588 result.modifier,
589 ParameterModifier::Substitute("old".to_string(), "new".to_string())
590 );
591 }
592
593 #[test]
594 fn test_parse_substitute_all() {
595 let result = parse_parameter_expansion("VAR//old/new").unwrap();
596 assert_eq!(result.var_name, "VAR");
597 assert_eq!(
598 result.modifier,
599 ParameterModifier::SubstituteAll("old".to_string(), "new".to_string())
600 );
601 }
602
603 #[test]
604 fn test_parse_indirect_prefix() {
605 let result = parse_parameter_expansion("!PREFIX*").unwrap();
606 assert_eq!(result.var_name, "!PREFIX");
607 assert_eq!(result.modifier, ParameterModifier::IndirectPrefix);
608 }
609
610 #[test]
611 fn test_parse_empty() {
612 let result = parse_parameter_expansion("");
613 assert!(result.is_err());
614 }
615
616 #[test]
617 fn test_parse_invalid_character() {
618 let result = parse_parameter_expansion("VAR@test");
619 assert!(result.is_err());
620 }
621
622 #[test]
623 fn test_expand_simple_variable() {
624 let mut shell_state = ShellState::new();
625 shell_state.set_var("TEST_VAR", "hello world".to_string());
626
627 let expansion = ParameterExpansion {
628 var_name: "TEST_VAR".to_string(),
629 modifier: ParameterModifier::None,
630 };
631
632 let result = expand_parameter(&expansion, &shell_state).unwrap();
633 assert_eq!(result, "hello world");
634 }
635
636 #[test]
637 fn test_expand_default_modifier() {
638 let mut shell_state = ShellState::new();
639 shell_state.set_var("TEST_VAR", "value".to_string());
640
641 let expansion = ParameterExpansion {
642 var_name: "TEST_VAR".to_string(),
643 modifier: ParameterModifier::Default("default".to_string()),
644 };
645
646 let result = expand_parameter(&expansion, &shell_state).unwrap();
647 assert_eq!(result, "value");
648 }
649
650 #[test]
651 fn test_expand_default_modifier_unset() {
652 let shell_state = ShellState::new();
653
654 let expansion = ParameterExpansion {
655 var_name: "UNSET_VAR".to_string(),
656 modifier: ParameterModifier::Default("default".to_string()),
657 };
658
659 let result = expand_parameter(&expansion, &shell_state).unwrap();
660 assert_eq!(result, "default");
661 }
662
663 #[test]
664 fn test_expand_substring() {
665 let mut shell_state = ShellState::new();
666 shell_state.set_var("TEST_VAR", "hello world".to_string());
667
668 let expansion = ParameterExpansion {
669 var_name: "TEST_VAR".to_string(),
670 modifier: ParameterModifier::Substring(6),
671 };
672
673 let result = expand_parameter(&expansion, &shell_state).unwrap();
674 assert_eq!(result, "world");
675 }
676}