Rust · 61661 bytes Raw Blame History
1 use pest::Parser;
2 use pest_derive::Parser;
3 use thiserror::Error;
4
5 use crate::ast::{
6 AndOrList, AndOrOp, Assignment, CaseClause, CaseStatement, CommandType, CompleteCommand,
7 CondExpr, ElifClause, ForStatement, FunctionDef, IfStatement, Pipeline, PipelineElement,
8 Redirect, SelectStatement, SimpleCommand, Statement, Subshell, VarExpansion, WhileStatement,
9 Word, WordPart,
10 };
11
12 #[derive(Parser)]
13 #[grammar = "grammar.pest"]
14 struct RushParser;
15
16 #[derive(Error, Debug)]
17 pub enum ParseError {
18 #[error("Parse error: {0}")]
19 PestError(#[from] Box<pest::error::Error<Rule>>),
20
21 #[error("Unexpected rule: {0:?}")]
22 UnexpectedRule(Rule),
23 }
24
25 /// Parse a line of shell input into a Statement
26 pub fn parse_line(input: &str) -> Result<Statement, ParseError> {
27 // Handle empty input or whitespace-only
28 if input.trim().is_empty() {
29 return Ok(Statement::Empty);
30 }
31
32 let mut pairs = RushParser::parse(Rule::input, input)
33 .map_err(Box::new)?;
34
35 let input_pair = pairs.next().ok_or_else(|| {
36 ParseError::PestError(Box::new(pest::error::Error::new_from_span(
37 pest::error::ErrorVariant::CustomError {
38 message: "No input parsed".to_string(),
39 },
40 pest::Span::new(input, 0, 0).unwrap(),
41 )))
42 })?;
43
44 for pair in input_pair.into_inner() {
45 match pair.as_rule() {
46 Rule::command_line => {
47 return parse_command_line(pair);
48 }
49 Rule::EOI => {}
50 _ => return Err(ParseError::UnexpectedRule(pair.as_rule())),
51 }
52 }
53
54 Ok(Statement::Empty)
55 }
56
57 fn parse_command_line(pair: pest::iterators::Pair<Rule>) -> Result<Statement, ParseError> {
58 let mut commands = Vec::new();
59
60 for inner_pair in pair.into_inner() {
61 match inner_pair.as_rule() {
62 Rule::complete_command => {
63 commands.push(parse_complete_command(inner_pair)?);
64 }
65 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
66 }
67 }
68
69 match commands.len() {
70 0 => Ok(Statement::Empty),
71 1 => Ok(Statement::Complete(commands.into_iter().next().unwrap())),
72 _ => Ok(Statement::Script(commands)),
73 }
74 }
75
76 fn parse_complete_command(pair: pest::iterators::Pair<Rule>) -> Result<CompleteCommand, ParseError> {
77 let mut inner_pairs = pair.into_inner();
78 let command_pair = inner_pairs.next().ok_or_else(|| {
79 ParseError::UnexpectedRule(Rule::complete_command)
80 })?;
81
82 // Check for background marker
83 let background = inner_pairs
84 .next()
85 .map(|p| p.as_rule() == Rule::background_marker)
86 .unwrap_or(false);
87
88 let command = match command_pair.as_rule() {
89 Rule::function_definition => CommandType::Function(parse_function_definition(command_pair)?),
90 Rule::if_statement => CommandType::If(parse_if_statement(command_pair)?),
91 Rule::while_statement => CommandType::While(parse_while_statement(command_pair)?),
92 Rule::for_statement => CommandType::For(parse_for_statement(command_pair)?),
93 Rule::select_statement => CommandType::Select(parse_select_statement(command_pair)?),
94 Rule::case_statement => CommandType::Case(parse_case_statement(command_pair)?),
95 Rule::and_or_list => parse_and_or_list_type(command_pair)?,
96 _ => return Err(ParseError::UnexpectedRule(command_pair.as_rule())),
97 };
98
99 Ok(CompleteCommand::new(command, background))
100 }
101
102 fn parse_and_or_list_type(pair: pest::iterators::Pair<Rule>) -> Result<CommandType, ParseError> {
103 let mut pipelines = Vec::new();
104 let mut operators = Vec::new();
105
106 for inner_pair in pair.into_inner() {
107 match inner_pair.as_rule() {
108 Rule::pipeline => {
109 pipelines.push(parse_pipeline(inner_pair)?);
110 }
111 Rule::and_or_op => {
112 let op_str = inner_pair.as_str();
113 operators.push(match op_str {
114 "&&" => AndOrOp::And,
115 "||" => AndOrOp::Or,
116 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
117 });
118 }
119 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
120 }
121 }
122
123 // If there's only one pipeline with no operators, return it directly
124 if pipelines.len() == 1 && operators.is_empty() {
125 let pipeline = pipelines.into_iter().next().unwrap();
126 // If it's a single-element pipeline, unwrap it
127 if pipeline.commands.len() == 1 {
128 let element = pipeline.commands.into_iter().next().unwrap();
129 return Ok(match element {
130 PipelineElement::Simple(cmd) => CommandType::Simple(cmd),
131 PipelineElement::Subshell(subshell) => CommandType::Subshell(subshell),
132 PipelineElement::ExtendedTest(cond) => CommandType::ExtendedTest(cond),
133 });
134 } else {
135 return Ok(CommandType::Pipeline(pipeline));
136 }
137 }
138
139 // Build the AndOrList
140 let mut pipelines_iter = pipelines.into_iter();
141 let first = pipelines_iter.next().unwrap();
142 let rest: Vec<(AndOrOp, Pipeline)> = operators
143 .into_iter()
144 .zip(pipelines_iter)
145 .collect();
146
147 Ok(CommandType::AndOrList(AndOrList::new(first, rest)))
148 }
149
150 fn parse_pipeline(pair: pest::iterators::Pair<Rule>) -> Result<Pipeline, ParseError> {
151 let mut elements = Vec::new();
152 let mut negated = false;
153
154 for inner_pair in pair.into_inner() {
155 match inner_pair.as_rule() {
156 Rule::pipeline_negation => {
157 negated = true;
158 }
159 Rule::pipeline_element => {
160 elements.push(parse_pipeline_element(inner_pair)?);
161 }
162 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
163 }
164 }
165
166 Ok(Pipeline::new_negated(elements, negated))
167 }
168
169 fn parse_pipeline_element(pair: pest::iterators::Pair<Rule>) -> Result<PipelineElement, ParseError> {
170 let inner_pair = pair.into_inner().next()
171 .ok_or_else(|| ParseError::UnexpectedRule(Rule::pipeline_element))?;
172
173 match inner_pair.as_rule() {
174 Rule::simple_command => {
175 Ok(PipelineElement::Simple(parse_simple_command(inner_pair)?))
176 }
177 Rule::subshell => {
178 Ok(PipelineElement::Subshell(parse_subshell(inner_pair)?))
179 }
180 Rule::extended_test => {
181 Ok(PipelineElement::ExtendedTest(parse_extended_test(inner_pair)?))
182 }
183 _ => Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
184 }
185 }
186
187 fn parse_extended_test(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
188 // extended_test = { "[[" ~ ws+ ~ cond_expr ~ ws+ ~ "]]" }
189 let inner = pair.into_inner().next()
190 .ok_or_else(|| ParseError::UnexpectedRule(Rule::extended_test))?;
191 parse_cond_expr(inner)
192 }
193
194 fn parse_cond_expr(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
195 // cond_expr = { cond_or }
196 let inner = pair.into_inner().next()
197 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_expr))?;
198 parse_cond_or(inner)
199 }
200
201 fn parse_cond_or(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
202 // cond_or = { cond_and ~ (ws* ~ "||" ~ ws* ~ cond_and)* }
203 let mut inner = pair.into_inner();
204
205 let first = inner.next()
206 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_or))?;
207 let mut result = parse_cond_and(first)?;
208
209 while let Some(next) = inner.next() {
210 let right = parse_cond_and(next)?;
211 result = CondExpr::Or(Box::new(result), Box::new(right));
212 }
213
214 Ok(result)
215 }
216
217 fn parse_cond_and(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
218 // cond_and = { cond_not ~ (ws* ~ "&&" ~ ws* ~ cond_not)* }
219 let mut inner = pair.into_inner();
220
221 let first = inner.next()
222 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_and))?;
223 let mut result = parse_cond_not(first)?;
224
225 while let Some(next) = inner.next() {
226 let right = parse_cond_not(next)?;
227 result = CondExpr::And(Box::new(result), Box::new(right));
228 }
229
230 Ok(result)
231 }
232
233 fn parse_cond_not(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
234 // cond_not = { ("!" ~ ws*)? ~ cond_primary }
235 let text = pair.as_str().trim();
236 let is_negated = text.starts_with('!');
237
238 let inner = pair.into_inner();
239 // Find the cond_primary
240 for child in inner {
241 if child.as_rule() == Rule::cond_primary {
242 let expr = parse_cond_primary(child)?;
243 return if is_negated {
244 Ok(CondExpr::Not(Box::new(expr)))
245 } else {
246 Ok(expr)
247 };
248 }
249 }
250
251 Err(ParseError::UnexpectedRule(Rule::cond_not))
252 }
253
254 fn parse_cond_primary(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
255 // cond_primary = {
256 // "(" ~ ws* ~ cond_expr ~ ws* ~ ")" // Grouping
257 // | cond_unary // -z, -n, -f, -d, etc.
258 // | cond_binary // string comparisons, regex
259 // | word // Single word (true if non-empty)
260 // }
261 let inner = pair.into_inner().next()
262 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_primary))?;
263
264 match inner.as_rule() {
265 Rule::cond_expr => parse_cond_expr(inner),
266 Rule::cond_unary => parse_cond_unary(inner),
267 Rule::cond_binary => parse_cond_binary(inner),
268 Rule::word => Ok(CondExpr::Word(parse_word(inner)?)),
269 _ => Err(ParseError::UnexpectedRule(inner.as_rule())),
270 }
271 }
272
273 fn parse_cond_unary(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
274 // cond_unary = { cond_unary_op ~ ws+ ~ word }
275 let mut inner = pair.into_inner();
276
277 let op = inner.next()
278 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_unary))?
279 .as_str()
280 .to_string();
281 let operand = inner.next()
282 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_unary))?;
283
284 Ok(CondExpr::Unary {
285 op,
286 operand: parse_word(operand)?,
287 })
288 }
289
290 fn parse_cond_binary(pair: pest::iterators::Pair<Rule>) -> Result<CondExpr, ParseError> {
291 // cond_binary = { word ~ ws+ ~ cond_binary_op ~ ws+ ~ word }
292 let mut inner = pair.into_inner();
293
294 let left = inner.next()
295 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_binary))?;
296 let op = inner.next()
297 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_binary))?
298 .as_str()
299 .to_string();
300 let right = inner.next()
301 .ok_or_else(|| ParseError::UnexpectedRule(Rule::cond_binary))?;
302
303 Ok(CondExpr::Binary {
304 left: parse_word(left)?,
305 op,
306 right: parse_word(right)?,
307 })
308 }
309
310 fn parse_simple_command(pair: pest::iterators::Pair<Rule>) -> Result<SimpleCommand, ParseError> {
311 let mut assignments = Vec::new();
312 let mut words = Vec::new();
313 let mut redirects = Vec::new();
314
315 for inner_pair in pair.into_inner() {
316 match inner_pair.as_rule() {
317 Rule::assignment => {
318 assignments.push(parse_assignment(inner_pair)?);
319 }
320 Rule::word => {
321 words.push(parse_word(inner_pair)?);
322 }
323 Rule::redirect => {
324 redirects.push(parse_redirect(inner_pair)?);
325 }
326 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
327 }
328 }
329
330 if redirects.is_empty() {
331 Ok(SimpleCommand::new(assignments, words))
332 } else {
333 Ok(SimpleCommand::with_redirects(assignments, words, redirects))
334 }
335 }
336
337 fn parse_assignment(pair: pest::iterators::Pair<Rule>) -> Result<Assignment, ParseError> {
338 let mut name = String::new();
339 let mut index = None;
340 let mut value = Word::new(vec![]);
341
342 for inner_pair in pair.into_inner() {
343 match inner_pair.as_rule() {
344 Rule::var_name => {
345 name = inner_pair.as_str().to_string();
346 }
347 Rule::array_index => {
348 // Extract the index from the brackets
349 for idx_pair in inner_pair.into_inner() {
350 if idx_pair.as_rule() == Rule::array_subscript {
351 index = Some(idx_pair.as_str().to_string());
352 }
353 }
354 }
355 Rule::word => {
356 value = parse_word(inner_pair)?;
357 }
358 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
359 }
360 }
361
362 if let Some(idx) = index {
363 Ok(Assignment::new_array(name, idx, value))
364 } else {
365 Ok(Assignment::new(name, value))
366 }
367 }
368
369 fn parse_word(pair: pest::iterators::Pair<Rule>) -> Result<Word, ParseError> {
370 let mut parts = Vec::new();
371
372 for part_pair in pair.into_inner() {
373 match part_pair.as_rule() {
374 Rule::word_part => {
375 parts.extend(parse_word_part(part_pair)?);
376 }
377 _ => return Err(ParseError::UnexpectedRule(part_pair.as_rule())),
378 }
379 }
380
381 Ok(Word::new(parts))
382 }
383
384 /// Recursively reconstruct command substitution content from parse tree
385 /// Handles nested parentheses: $(echo $(echo inner))
386 fn reconstruct_command_subst_content(pair: pest::iterators::Pair<Rule>) -> String {
387 match pair.as_rule() {
388 Rule::command_subst_content => {
389 // Recursively reconstruct all parts
390 pair.into_inner()
391 .map(|p| reconstruct_command_subst_content(p))
392 .collect::<String>()
393 }
394 Rule::command_subst_part => {
395 // A part is either nested parens or text
396 pair.into_inner()
397 .map(|p| reconstruct_command_subst_content(p))
398 .collect::<String>()
399 }
400 Rule::nested_parens => {
401 // Add the parentheses back
402 let content = pair.into_inner()
403 .map(|p| reconstruct_command_subst_content(p))
404 .collect::<String>();
405 format!("({})", content)
406 }
407 Rule::command_subst_text => {
408 // Just return the text as-is
409 pair.as_str().to_string()
410 }
411 _ => {
412 // For any other rule, just return the text
413 pair.as_str().to_string()
414 }
415 }
416 }
417
418 /// Unescape backticks in backtick command substitution content
419 /// Converts \` to ` for nested backtick handling: `echo \`inner\`` -> echo `inner`
420 fn unescape_backticks(s: &str) -> String {
421 let mut result = String::with_capacity(s.len());
422 let mut chars = s.chars().peekable();
423 while let Some(ch) = chars.next() {
424 if ch == '\\' {
425 if let Some(&next) = chars.peek() {
426 if next == '`' {
427 // Unescape backtick
428 result.push(chars.next().unwrap());
429 } else {
430 // Keep other escape sequences
431 result.push(ch);
432 }
433 } else {
434 result.push(ch);
435 }
436 } else {
437 result.push(ch);
438 }
439 }
440 result
441 }
442
443 fn parse_word_part(pair: pest::iterators::Pair<Rule>) -> Result<Vec<WordPart>, ParseError> {
444 let inner = pair.into_inner().next().ok_or_else(|| {
445 ParseError::UnexpectedRule(Rule::word_part)
446 })?;
447
448 match inner.as_rule() {
449 Rule::bare_word_part => {
450 Ok(vec![WordPart::Literal(inner.as_str().to_string())])
451 }
452 Rule::extglob_pattern => {
453 // Extended glob patterns like !(*.txt) are treated as literals for glob expansion
454 Ok(vec![WordPart::Literal(inner.as_str().to_string())])
455 }
456 Rule::var_expansion => {
457 Ok(vec![WordPart::VarExpansion(parse_var_expansion(inner)?)])
458 }
459 Rule::arithmetic_expansion => {
460 let content = inner.into_inner().next()
461 .ok_or_else(|| ParseError::UnexpectedRule(Rule::arithmetic_expansion))?
462 .as_str()
463 .to_string();
464 Ok(vec![WordPart::ArithmeticExpansion(content)])
465 }
466 Rule::command_substitution => {
467 // Reconstruct the content from the parsed parts (handles nested parens)
468 let content = inner.into_inner()
469 .map(|p| reconstruct_command_subst_content(p))
470 .collect::<String>();
471 Ok(vec![WordPart::CommandSubstitution(content)])
472 }
473 Rule::backtick_substitution => {
474 // Extract command from backticks: `command`
475 // Handle escaped backticks for nesting: `echo \`inner\`` -> echo `inner`
476 let raw_content = inner.into_inner().next()
477 .ok_or_else(|| ParseError::UnexpectedRule(Rule::backtick_substitution))?
478 .as_str();
479 let content = unescape_backticks(raw_content);
480 Ok(vec![WordPart::CommandSubstitution(content)])
481 }
482 Rule::quoted_string => {
483 parse_quoted_string(inner)
484 }
485 Rule::array_literal => {
486 let mut elements = Vec::new();
487 for elem_pair in inner.into_inner() {
488 if elem_pair.as_rule() == Rule::word {
489 elements.push(parse_word(elem_pair)?);
490 }
491 }
492 Ok(vec![WordPart::ArrayLiteral(elements)])
493 }
494 _ => Err(ParseError::UnexpectedRule(inner.as_rule())),
495 }
496 }
497
498 fn parse_var_expansion(pair: pest::iterators::Pair<Rule>) -> Result<VarExpansion, ParseError> {
499 // Get the original text before consuming the pair
500 let original = pair.as_str();
501 let is_braced = original.starts_with("${");
502
503 // Check for ${#arr[@]} or ${#arr[*]} - array length
504 if original.starts_with("${#") && (original.contains("[@]") || original.contains("[*]")) {
505 let mut inner = pair.into_inner();
506 let var_name = inner.next()
507 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
508 .as_str()
509 .to_string();
510 return Ok(VarExpansion::ArrayLength(var_name));
511 }
512
513 // Check for ${!arr[@]} or ${!arr[*]} - array indices
514 if original.starts_with("${!") && (original.contains("[@]") || original.contains("[*]")) {
515 let mut inner = pair.into_inner();
516 let var_name = inner.next()
517 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
518 .as_str()
519 .to_string();
520 return Ok(VarExpansion::ArrayIndices(var_name));
521 }
522
523 // Check for ${!var} - indirect expansion (without array subscript)
524 if original.starts_with("${!") && !original.contains('[') {
525 let mut inner = pair.into_inner();
526 let var_name = inner.next()
527 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
528 .as_str()
529 .to_string();
530 return Ok(VarExpansion::Indirect(var_name));
531 }
532
533 // Check for ${#VAR} - variable length
534 if original.starts_with("${#") {
535 let mut inner = pair.into_inner();
536 let var_name = inner.next()
537 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
538 .as_str()
539 .to_string();
540 return Ok(VarExpansion::Length(var_name));
541 }
542
543 // Check for ${arr[@]} - all elements
544 if original.contains("[@]") {
545 let mut inner = pair.into_inner();
546 let var_name = inner.next()
547 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
548 .as_str()
549 .to_string();
550 return Ok(VarExpansion::ArrayAll(var_name));
551 }
552
553 // Check for ${arr[*]} - all elements as single word
554 if original.contains("[*]") {
555 let mut inner = pair.into_inner();
556 let var_name = inner.next()
557 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
558 .as_str()
559 .to_string();
560 return Ok(VarExpansion::ArrayStar(var_name));
561 }
562
563 // Check for ${arr[index]} - array element
564 if original.contains('[') && original.contains(']') {
565 let mut inner = pair.into_inner();
566 let var_name = inner.next()
567 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
568 .as_str()
569 .to_string();
570 let index = inner.next()
571 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_expansion))?
572 .as_str()
573 .to_string();
574 return Ok(VarExpansion::ArrayElement { name: var_name, index });
575 }
576
577 let mut inner = pair.into_inner();
578 let first = inner.next().ok_or_else(|| {
579 ParseError::UnexpectedRule(Rule::var_expansion)
580 })?;
581
582 match first.as_rule() {
583 Rule::var_name => {
584 let var_name = first.as_str().to_string();
585
586 // Check if there's a modifier
587 if let Some(modifier_pair) = inner.next() {
588 if modifier_pair.as_rule() == Rule::var_modifier {
589 return parse_var_modifier(&var_name, modifier_pair);
590 }
591 }
592
593 // Determine if it's simple or braced
594 if is_braced {
595 Ok(VarExpansion::Braced(var_name))
596 } else {
597 Ok(VarExpansion::Simple(var_name))
598 }
599 }
600 Rule::special_var => {
601 // Special variables: $?, $#, $@, $*, $0, $1, etc.
602 // These are always treated as simple expansions
603 let var_name = first.as_str().to_string();
604 Ok(VarExpansion::Simple(var_name))
605 }
606 _ => Err(ParseError::UnexpectedRule(first.as_rule())),
607 }
608 }
609
610 fn parse_var_modifier(var_name: &str, pair: pest::iterators::Pair<Rule>) -> Result<VarExpansion, ParseError> {
611 let modifier_text = pair.as_str();
612
613 if modifier_text.starts_with(":-") {
614 // ${VAR:-default}
615 let default_word = pair.into_inner().next()
616 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
617 let default = parse_word(default_word)?;
618 return Ok(VarExpansion::WithDefault {
619 name: var_name.to_string(),
620 default: Box::new(default),
621 });
622 }
623
624 if modifier_text.starts_with(":=") {
625 // ${VAR:=default} - assign default if unset/empty
626 let default_word = pair.into_inner().next()
627 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
628 let default = parse_word(default_word)?;
629 return Ok(VarExpansion::AssignDefault {
630 name: var_name.to_string(),
631 default: Box::new(default),
632 });
633 }
634
635 if modifier_text.starts_with(":+") {
636 // ${VAR:+alternate} - use alternate if set and non-empty
637 let alternate_word = pair.into_inner().next()
638 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
639 let alternate = parse_word(alternate_word)?;
640 return Ok(VarExpansion::UseIfSet {
641 name: var_name.to_string(),
642 alternate: Box::new(alternate),
643 });
644 }
645
646 if modifier_text.starts_with(":?") {
647 // ${VAR:?message} - error if unset/empty
648 let message_word = pair.into_inner().next()
649 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
650 let message = parse_word(message_word)?;
651 return Ok(VarExpansion::ErrorIfUnset {
652 name: var_name.to_string(),
653 message: Box::new(message),
654 });
655 }
656
657 // Non-colon variants (check unset only, not empty)
658 if modifier_text.starts_with('-') && !modifier_text.starts_with("--") {
659 // ${VAR-default} - use default only if unset
660 let default_word = pair.into_inner().next()
661 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
662 let default = parse_word(default_word)?;
663 return Ok(VarExpansion::WithDefaultUnsetOnly {
664 name: var_name.to_string(),
665 default: Box::new(default),
666 });
667 }
668
669 if modifier_text.starts_with('=') && !modifier_text.starts_with("==") {
670 // ${VAR=default} - assign only if unset
671 let default_word = pair.into_inner().next()
672 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
673 let default = parse_word(default_word)?;
674 return Ok(VarExpansion::AssignDefaultUnsetOnly {
675 name: var_name.to_string(),
676 default: Box::new(default),
677 });
678 }
679
680 if modifier_text.starts_with('+') {
681 // ${VAR+alternate} - use if set (even if empty)
682 let alternate_word = pair.into_inner().next()
683 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
684 let alternate = parse_word(alternate_word)?;
685 return Ok(VarExpansion::UseIfSetOnly {
686 name: var_name.to_string(),
687 alternate: Box::new(alternate),
688 });
689 }
690
691 if modifier_text.starts_with('?') {
692 // ${VAR?message} - error only if unset
693 let message_word = pair.into_inner().next()
694 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?;
695 let message = parse_word(message_word)?;
696 return Ok(VarExpansion::ErrorIfUnsetOnly {
697 name: var_name.to_string(),
698 message: Box::new(message),
699 });
700 }
701
702 let mut inner = pair.into_inner();
703
704 // Check the pattern
705 if modifier_text.starts_with("##") {
706 // ${VAR##pattern}
707 let pattern = inner.next()
708 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
709 .as_str()
710 .to_string();
711 return Ok(VarExpansion::RemoveLongestPrefix {
712 name: var_name.to_string(),
713 pattern,
714 });
715 } else if modifier_text.starts_with('#') {
716 // ${VAR#pattern}
717 let pattern = inner.next()
718 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
719 .as_str()
720 .to_string();
721 return Ok(VarExpansion::RemoveShortestPrefix {
722 name: var_name.to_string(),
723 pattern,
724 });
725 } else if modifier_text.starts_with("%%") {
726 // ${VAR%%pattern}
727 let pattern = inner.next()
728 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
729 .as_str()
730 .to_string();
731 return Ok(VarExpansion::RemoveLongestSuffix {
732 name: var_name.to_string(),
733 pattern,
734 });
735 } else if modifier_text.starts_with('%') {
736 // ${VAR%pattern}
737 let pattern = inner.next()
738 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
739 .as_str()
740 .to_string();
741 return Ok(VarExpansion::RemoveShortestSuffix {
742 name: var_name.to_string(),
743 pattern,
744 });
745 } else if modifier_text.starts_with("//") {
746 // ${VAR//pattern/replacement}
747 let pattern = inner.next()
748 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
749 .as_str()
750 .to_string();
751 let replacement = inner.next()
752 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
753 .as_str()
754 .to_string();
755 return Ok(VarExpansion::ReplaceAll {
756 name: var_name.to_string(),
757 pattern,
758 replacement,
759 });
760 } else if modifier_text.starts_with('/') {
761 // ${VAR/pattern/replacement}
762 let pattern = inner.next()
763 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
764 .as_str()
765 .to_string();
766 let replacement = inner.next()
767 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
768 .as_str()
769 .to_string();
770 return Ok(VarExpansion::ReplaceFirst {
771 name: var_name.to_string(),
772 pattern,
773 replacement,
774 });
775 } else if modifier_text.starts_with(':') {
776 // ${VAR:offset} or ${VAR:offset:length}
777 let offset = inner.next()
778 .ok_or_else(|| ParseError::UnexpectedRule(Rule::var_modifier))?
779 .as_str()
780 .parse::<i32>()
781 .map_err(|_| ParseError::UnexpectedRule(Rule::var_offset))?;
782 let length = inner.next().map(|l| l.as_str().parse::<usize>().ok()).flatten();
783 return Ok(VarExpansion::Substring {
784 name: var_name.to_string(),
785 offset,
786 length,
787 });
788 } else if modifier_text == "^^" {
789 return Ok(VarExpansion::UppercaseAll(var_name.to_string()));
790 } else if modifier_text == "^" {
791 return Ok(VarExpansion::UppercaseFirst(var_name.to_string()));
792 } else if modifier_text == ",," {
793 return Ok(VarExpansion::LowercaseAll(var_name.to_string()));
794 } else if modifier_text == "," {
795 return Ok(VarExpansion::LowercaseFirst(var_name.to_string()));
796 } else if modifier_text.starts_with('@') && modifier_text.len() == 2 {
797 // ${VAR@Q}, ${VAR@E}, etc.
798 let op = modifier_text.chars().nth(1).unwrap();
799 return Ok(VarExpansion::Transform {
800 name: var_name.to_string(),
801 op,
802 });
803 }
804
805 Err(ParseError::UnexpectedRule(Rule::var_modifier))
806 }
807
808 fn parse_quoted_string(pair: pest::iterators::Pair<Rule>) -> Result<Vec<WordPart>, ParseError> {
809 let inner = pair.into_inner().next().ok_or_else(|| {
810 ParseError::UnexpectedRule(Rule::quoted_string)
811 })?;
812
813 match inner.as_rule() {
814 Rule::single_quoted => {
815 // Single quotes: literal (strip quotes)
816 let s = inner.as_str();
817 let content = &s[1..s.len() - 1];
818 Ok(vec![WordPart::Literal(content.to_string())])
819 }
820 Rule::double_quoted => {
821 // Double quotes: can contain expansions
822 parse_double_quoted(inner)
823 }
824 _ => Err(ParseError::UnexpectedRule(inner.as_rule())),
825 }
826 }
827
828 fn parse_double_quoted(pair: pest::iterators::Pair<Rule>) -> Result<Vec<WordPart>, ParseError> {
829 let mut parts = Vec::new();
830
831 for inner_pair in pair.into_inner() {
832 match inner_pair.as_rule() {
833 Rule::double_quoted_content => {
834 for content_part in inner_pair.into_inner() {
835 match content_part.as_rule() {
836 Rule::double_quoted_part => {
837 parts.extend(parse_double_quoted_part(content_part)?);
838 }
839 _ => return Err(ParseError::UnexpectedRule(content_part.as_rule())),
840 }
841 }
842 }
843 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
844 }
845 }
846
847 Ok(parts)
848 }
849
850 fn parse_double_quoted_part(pair: pest::iterators::Pair<Rule>) -> Result<Vec<WordPart>, ParseError> {
851 let inner = pair.into_inner().next().ok_or_else(|| {
852 ParseError::UnexpectedRule(Rule::double_quoted_part)
853 })?;
854
855 match inner.as_rule() {
856 Rule::double_quoted_text => {
857 Ok(vec![WordPart::Literal(inner.as_str().to_string())])
858 }
859 Rule::var_expansion => {
860 Ok(vec![WordPart::VarExpansion(parse_var_expansion(inner)?)])
861 }
862 Rule::arithmetic_expansion => {
863 let content = inner.into_inner().next()
864 .ok_or_else(|| ParseError::UnexpectedRule(Rule::arithmetic_expansion))?
865 .as_str()
866 .to_string();
867 Ok(vec![WordPart::ArithmeticExpansion(content)])
868 }
869 Rule::command_substitution => {
870 // Reconstruct the content from the parsed parts (handles nested parens)
871 let content = inner.into_inner()
872 .map(|p| reconstruct_command_subst_content(p))
873 .collect::<String>();
874 Ok(vec![WordPart::CommandSubstitution(content)])
875 }
876 Rule::backtick_substitution => {
877 // Extract command from backticks: `command`
878 // Handle escaped backticks for nesting: `echo \`inner\`` -> echo `inner`
879 let raw_content = inner.into_inner().next()
880 .ok_or_else(|| ParseError::UnexpectedRule(Rule::backtick_substitution))?
881 .as_str();
882 let content = unescape_backticks(raw_content);
883 Ok(vec![WordPart::CommandSubstitution(content)])
884 }
885 _ => Err(ParseError::UnexpectedRule(inner.as_rule())),
886 }
887 }
888
889 fn parse_redirect(pair: pest::iterators::Pair<Rule>) -> Result<Redirect, ParseError> {
890 let inner = pair.into_inner().next().ok_or_else(|| {
891 ParseError::UnexpectedRule(Rule::redirect)
892 })?;
893
894 match inner.as_rule() {
895 Rule::redirect_input => {
896 let word = inner.into_inner().next()
897 .ok_or_else(|| ParseError::UnexpectedRule(Rule::redirect_input))?;
898 Ok(Redirect::Input {
899 file: parse_word(word)?,
900 })
901 }
902 Rule::redirect_output => {
903 let mut fd = None;
904 let mut file_word = None;
905
906 for inner_pair in inner.into_inner() {
907 match inner_pair.as_rule() {
908 Rule::fd_number => {
909 fd = Some(inner_pair.as_str().parse::<u32>().map_err(|_| {
910 ParseError::UnexpectedRule(Rule::fd_number)
911 })?);
912 }
913 Rule::word => {
914 file_word = Some(parse_word(inner_pair)?);
915 }
916 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
917 }
918 }
919
920 Ok(Redirect::Output {
921 fd,
922 file: file_word.ok_or_else(|| ParseError::UnexpectedRule(Rule::redirect_output))?,
923 })
924 }
925 Rule::redirect_output_append => {
926 let mut fd = None;
927 let mut file_word = None;
928
929 for inner_pair in inner.into_inner() {
930 match inner_pair.as_rule() {
931 Rule::fd_number => {
932 fd = Some(inner_pair.as_str().parse::<u32>().map_err(|_| {
933 ParseError::UnexpectedRule(Rule::fd_number)
934 })?);
935 }
936 Rule::word => {
937 file_word = Some(parse_word(inner_pair)?);
938 }
939 _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
940 }
941 }
942
943 Ok(Redirect::OutputAppend {
944 fd,
945 file: file_word.ok_or_else(|| ParseError::UnexpectedRule(Rule::redirect_output_append))?,
946 })
947 }
948 Rule::redirect_stderr_to_stdout => {
949 Ok(Redirect::StderrToStdout)
950 }
951 Rule::redirect_all_output => {
952 // Check if it's &>> (append) or &> (truncate)
953 let text = inner.as_str();
954 let append = text.starts_with("&>>");
955
956 let word = inner.into_inner().next()
957 .ok_or_else(|| ParseError::UnexpectedRule(Rule::redirect_all_output))?;
958
959 Ok(Redirect::AllOutput {
960 file: parse_word(word)?,
961 append,
962 })
963 }
964 Rule::redirect_heredoc => {
965 // Parse heredoc marker: <<EOF or <<-EOF
966 let text = inner.as_str();
967 let strip_tabs = text.starts_with("<<-");
968
969 let heredoc_delimiter_pair = inner.into_inner().next()
970 .ok_or_else(|| ParseError::UnexpectedRule(Rule::redirect_heredoc))?;
971
972 // heredoc_delimiter contains either quoted_string or bare_delimiter
973 let delimiter_pair = heredoc_delimiter_pair.into_inner().next()
974 .ok_or_else(|| ParseError::UnexpectedRule(Rule::heredoc_delimiter))?;
975
976 // Extract delimiter and check if quoted (determines expansion)
977 let (delimiter, expand) = match delimiter_pair.as_rule() {
978 Rule::quoted_string => {
979 // Quoted delimiter means no expansion
980 let s = delimiter_pair.as_str();
981 // Remove quotes
982 let delim = if s.starts_with('"') || s.starts_with('\'') {
983 &s[1..s.len()-1]
984 } else {
985 s
986 };
987 (delim.to_string(), false)
988 }
989 Rule::bare_delimiter => {
990 // Unquoted delimiter means expand
991 (delimiter_pair.as_str().to_string(), true)
992 }
993 _ => return Err(ParseError::UnexpectedRule(delimiter_pair.as_rule())),
994 };
995
996 // Content will be collected separately (requires multi-line parsing)
997 Ok(Redirect::Heredoc {
998 delimiter,
999 content: Vec::new(), // Empty for now
1000 strip_tabs,
1001 expand,
1002 })
1003 }
1004 Rule::redirect_herestring => {
1005 // Parse herestring: <<<word
1006 let word = inner.into_inner().next()
1007 .ok_or_else(|| ParseError::UnexpectedRule(Rule::redirect_herestring))?;
1008
1009 Ok(Redirect::Herestring {
1010 content: parse_word(word)?,
1011 })
1012 }
1013 Rule::process_subst_input => {
1014 // Parse process substitution: <(command)
1015 // Reconstruct the command content (handles nested parens)
1016 let command = inner.into_inner()
1017 .map(|p| reconstruct_command_subst_content(p))
1018 .collect::<String>();
1019
1020 Ok(Redirect::ProcessSubstInput { command })
1021 }
1022 Rule::process_subst_output => {
1023 // Parse process substitution: >(command)
1024 // Reconstruct the command content (handles nested parens)
1025 let command = inner.into_inner()
1026 .map(|p| reconstruct_command_subst_content(p))
1027 .collect::<String>();
1028
1029 Ok(Redirect::ProcessSubstOutput { command })
1030 }
1031 _ => Err(ParseError::UnexpectedRule(inner.as_rule())),
1032 }
1033 }
1034
1035 fn parse_if_statement(pair: pest::iterators::Pair<Rule>) -> Result<IfStatement, ParseError> {
1036 let mut condition = None;
1037 let mut then_body = Vec::new();
1038 let mut elif_clauses = Vec::new();
1039 let mut else_body = None;
1040
1041 for inner_pair in pair.into_inner() {
1042 match inner_pair.as_rule() {
1043 Rule::complete_command => {
1044 if condition.is_none() {
1045 condition = Some(Box::new(parse_complete_command(inner_pair)?));
1046 }
1047 }
1048 Rule::command_list => {
1049 if condition.is_some() && then_body.is_empty() {
1050 then_body = parse_command_list(inner_pair)?;
1051 }
1052 }
1053 Rule::elif_clause => {
1054 elif_clauses.push(parse_elif_clause(inner_pair)?);
1055 }
1056 Rule::else_clause => {
1057 else_body = Some(parse_else_clause(inner_pair)?);
1058 }
1059 Rule::NEWLINE => {}, // Ignore newlines
1060 _ => {}, // Ignore keywords like "if", "then", "fi"
1061 }
1062 }
1063
1064 Ok(IfStatement::new(
1065 condition.ok_or_else(|| ParseError::UnexpectedRule(Rule::if_statement))?,
1066 then_body,
1067 elif_clauses,
1068 else_body,
1069 ))
1070 }
1071
1072 fn parse_elif_clause(pair: pest::iterators::Pair<Rule>) -> Result<ElifClause, ParseError> {
1073 let mut condition = None;
1074 let mut then_body = Vec::new();
1075
1076 for inner_pair in pair.into_inner() {
1077 match inner_pair.as_rule() {
1078 Rule::complete_command => {
1079 condition = Some(Box::new(parse_complete_command(inner_pair)?));
1080 }
1081 Rule::command_list => {
1082 then_body = parse_command_list(inner_pair)?;
1083 }
1084 Rule::NEWLINE => {},
1085 _ => {},
1086 }
1087 }
1088
1089 Ok(ElifClause::new(
1090 condition.ok_or_else(|| ParseError::UnexpectedRule(Rule::elif_clause))?,
1091 then_body,
1092 ))
1093 }
1094
1095 fn parse_else_clause(pair: pest::iterators::Pair<Rule>) -> Result<Vec<CompleteCommand>, ParseError> {
1096 for inner_pair in pair.into_inner() {
1097 match inner_pair.as_rule() {
1098 Rule::command_list => {
1099 return parse_command_list(inner_pair);
1100 }
1101 Rule::NEWLINE => {},
1102 _ => {},
1103 }
1104 }
1105 Ok(Vec::new())
1106 }
1107
1108 fn parse_while_statement(pair: pest::iterators::Pair<Rule>) -> Result<WhileStatement, ParseError> {
1109 let mut condition = None;
1110 let mut body = Vec::new();
1111
1112 for inner_pair in pair.into_inner() {
1113 match inner_pair.as_rule() {
1114 Rule::complete_command => {
1115 condition = Some(Box::new(parse_complete_command(inner_pair)?));
1116 }
1117 Rule::command_list => {
1118 body = parse_command_list(inner_pair)?;
1119 }
1120 Rule::NEWLINE => {},
1121 _ => {},
1122 }
1123 }
1124
1125 Ok(WhileStatement::new(
1126 condition.ok_or_else(|| ParseError::UnexpectedRule(Rule::while_statement))?,
1127 body,
1128 ))
1129 }
1130
1131 fn parse_for_statement(pair: pest::iterators::Pair<Rule>) -> Result<ForStatement, ParseError> {
1132 let mut var_name = String::new();
1133 let mut words = Vec::new();
1134 let mut body = Vec::new();
1135
1136 for inner_pair in pair.into_inner() {
1137 match inner_pair.as_rule() {
1138 Rule::var_name => {
1139 var_name = inner_pair.as_str().to_string();
1140 }
1141 Rule::word => {
1142 words.push(parse_word(inner_pair)?);
1143 }
1144 Rule::command_list => {
1145 body = parse_command_list(inner_pair)?;
1146 }
1147 Rule::NEWLINE => {},
1148 _ => {},
1149 }
1150 }
1151
1152 Ok(ForStatement::new(var_name, words, body))
1153 }
1154
1155 fn parse_select_statement(pair: pest::iterators::Pair<Rule>) -> Result<SelectStatement, ParseError> {
1156 let mut var_name = String::new();
1157 let mut words = Vec::new();
1158 let mut body = Vec::new();
1159
1160 for inner_pair in pair.into_inner() {
1161 match inner_pair.as_rule() {
1162 Rule::var_name => {
1163 var_name = inner_pair.as_str().to_string();
1164 }
1165 Rule::word => {
1166 words.push(parse_word(inner_pair)?);
1167 }
1168 Rule::command_list => {
1169 body = parse_command_list(inner_pair)?;
1170 }
1171 Rule::NEWLINE => {},
1172 _ => {},
1173 }
1174 }
1175
1176 Ok(SelectStatement { var_name, words, body })
1177 }
1178
1179 fn parse_case_statement(pair: pest::iterators::Pair<Rule>) -> Result<CaseStatement, ParseError> {
1180 let mut word = None;
1181 let mut clauses = Vec::new();
1182
1183 for inner_pair in pair.into_inner() {
1184 match inner_pair.as_rule() {
1185 Rule::word => {
1186 if word.is_none() {
1187 word = Some(parse_word(inner_pair)?);
1188 }
1189 }
1190 Rule::case_clause => {
1191 clauses.push(parse_case_clause(inner_pair)?);
1192 }
1193 Rule::NEWLINE => {},
1194 _ => {},
1195 }
1196 }
1197
1198 Ok(CaseStatement::new(
1199 word.ok_or_else(|| ParseError::UnexpectedRule(Rule::case_statement))?,
1200 clauses,
1201 ))
1202 }
1203
1204 fn parse_case_clause(pair: pest::iterators::Pair<Rule>) -> Result<CaseClause, ParseError> {
1205 let mut patterns = Vec::new();
1206 let mut body = Vec::new();
1207
1208 for inner_pair in pair.into_inner() {
1209 match inner_pair.as_rule() {
1210 Rule::pattern => {
1211 // Pattern contains a word
1212 for pattern_part in inner_pair.into_inner() {
1213 if pattern_part.as_rule() == Rule::word {
1214 patterns.push(parse_word(pattern_part)?);
1215 }
1216 }
1217 }
1218 Rule::command_list => {
1219 body = parse_command_list(inner_pair)?;
1220 }
1221 Rule::NEWLINE => {},
1222 _ => {},
1223 }
1224 }
1225
1226 Ok(CaseClause::new(patterns, body))
1227 }
1228
1229 fn parse_function_definition(pair: pest::iterators::Pair<Rule>) -> Result<FunctionDef, ParseError> {
1230 let mut name = None;
1231 let mut body = Vec::new();
1232
1233 for inner_pair in pair.into_inner() {
1234 match inner_pair.as_rule() {
1235 Rule::function_name => {
1236 name = Some(inner_pair.as_str().to_string());
1237 }
1238 Rule::command_list => {
1239 body = parse_command_list(inner_pair)?;
1240 }
1241 Rule::NEWLINE => {},
1242 _ => {},
1243 }
1244 }
1245
1246 Ok(FunctionDef::new(
1247 name.ok_or_else(|| ParseError::UnexpectedRule(Rule::function_definition))?,
1248 body,
1249 ))
1250 }
1251
1252 fn parse_subshell(pair: pest::iterators::Pair<Rule>) -> Result<Subshell, ParseError> {
1253 let mut commands = Vec::new();
1254
1255 for inner_pair in pair.into_inner() {
1256 match inner_pair.as_rule() {
1257 Rule::command_list => {
1258 commands = parse_command_list(inner_pair)?;
1259 }
1260 Rule::NEWLINE => {},
1261 _ => {},
1262 }
1263 }
1264
1265 Ok(Subshell::new(commands))
1266 }
1267
1268 fn parse_command_list(pair: pest::iterators::Pair<Rule>) -> Result<Vec<CompleteCommand>, ParseError> {
1269 let mut commands = Vec::new();
1270
1271 for inner_pair in pair.into_inner() {
1272 match inner_pair.as_rule() {
1273 Rule::complete_command => {
1274 commands.push(parse_complete_command(inner_pair)?);
1275 }
1276 Rule::NEWLINE => {},
1277 _ => {},
1278 }
1279 }
1280
1281 Ok(commands)
1282 }
1283
1284 #[cfg(test)]
1285 mod tests {
1286 use super::*;
1287
1288 #[test]
1289 fn test_parse_simple_command() {
1290 let result = parse_line("ls").unwrap();
1291 match result {
1292 Statement::Complete(complete_cmd) => {
1293 if let CommandType::Simple(cmd) = &complete_cmd.command {
1294 assert_eq!(cmd.assignments.len(), 0);
1295 assert_eq!(cmd.words.len(), 1);
1296 assert!(cmd.words[0].is_literal());
1297 } else {
1298 panic!("Expected Simple or Pipeline command");
1299 }
1300 }
1301 _ => panic!("Expected Simple command"),
1302 }
1303 }
1304
1305 #[test]
1306 fn test_parse_with_variable() {
1307 let result = parse_line("echo $USER").unwrap();
1308 match result {
1309 Statement::Complete(complete_cmd) => {
1310 if let CommandType::Simple(cmd) = &complete_cmd.command {
1311 assert_eq!(cmd.words.len(), 2);
1312 // First word: "echo"
1313 assert!(cmd.words[0].is_literal());
1314 // Second word: $USER
1315 assert_eq!(cmd.words[1].parts.len(), 1);
1316 match &cmd.words[1].parts[0] {
1317 WordPart::VarExpansion(VarExpansion::Simple(name)) => {
1318 assert_eq!(name, "USER");
1319 }
1320 _ => panic!("Expected variable expansion"),
1321 }
1322 } else {
1323 panic!("Expected Simple or Pipeline command");
1324 }
1325 }
1326 _ => panic!("Expected Simple command"),
1327 }
1328 }
1329
1330 #[test]
1331 fn test_parse_assignment() {
1332 let result = parse_line("FOO=bar").unwrap();
1333 match result {
1334 Statement::Complete(complete_cmd) => {
1335 if let CommandType::Simple(cmd) = &complete_cmd.command {
1336 assert_eq!(cmd.assignments.len(), 1);
1337 assert_eq!(cmd.assignments[0].name, "FOO");
1338 assert!(cmd.assignments[0].value.is_literal());
1339 } else {
1340 panic!("Expected Simple or Pipeline command");
1341 }
1342 }
1343 _ => panic!("Expected Simple command"),
1344 }
1345 }
1346
1347 #[test]
1348 fn test_parse_assignment_with_command() {
1349 let result = parse_line("FOO=bar echo test").unwrap();
1350 match result {
1351 Statement::Complete(complete_cmd) => {
1352 if let CommandType::Simple(cmd) = &complete_cmd.command {
1353 assert_eq!(cmd.assignments.len(), 1);
1354 assert_eq!(cmd.words.len(), 2);
1355 } else {
1356 panic!("Expected Simple or Pipeline command");
1357 }
1358 }
1359 _ => panic!("Expected Simple command"),
1360 }
1361 }
1362
1363 #[test]
1364 fn test_parse_braced_var() {
1365 let result = parse_line("echo ${VAR}").unwrap();
1366 match result {
1367 Statement::Complete(complete_cmd) => {
1368 if let CommandType::Simple(cmd) = &complete_cmd.command {
1369 match &cmd.words[1].parts[0] {
1370 WordPart::VarExpansion(VarExpansion::Braced(name)) => {
1371 assert_eq!(name, "VAR");
1372 }
1373 _ => panic!("Expected braced variable expansion"),
1374 }
1375 } else {
1376 panic!("Expected Simple or Pipeline command");
1377 }
1378 }
1379 _ => panic!("Expected Simple command"),
1380 }
1381 }
1382
1383 #[test]
1384 fn test_parse_command_substitution() {
1385 let result = parse_line("echo $(pwd)").unwrap();
1386 match result {
1387 Statement::Complete(complete_cmd) => {
1388 if let CommandType::Simple(cmd) = &complete_cmd.command {
1389 match &cmd.words[1].parts[0] {
1390 WordPart::CommandSubstitution(content) => {
1391 assert_eq!(content, "pwd");
1392 }
1393 _ => panic!("Expected command substitution"),
1394 }
1395 } else {
1396 panic!("Expected Simple or Pipeline command");
1397 }
1398 }
1399 _ => panic!("Expected Simple command"),
1400 }
1401 }
1402
1403 #[test]
1404 fn test_parse_simple_pipeline() {
1405 let result = parse_line("ls | grep test").unwrap();
1406 match result {
1407 Statement::Complete(complete_cmd) => {
1408 if let CommandType::Pipeline(pipeline) = &complete_cmd.command {
1409 assert_eq!(pipeline.commands.len(), 2);
1410 if let PipelineElement::Simple(cmd) = &pipeline.commands[0] {
1411 assert_eq!(cmd.words.len(), 1);
1412 } else {
1413 panic!("Expected Simple command");
1414 }
1415 if let PipelineElement::Simple(cmd) = &pipeline.commands[1] {
1416 assert_eq!(cmd.words.len(), 2);
1417 } else {
1418 panic!("Expected Simple command");
1419 }
1420 } else {
1421 panic!("Expected Simple or Pipeline command");
1422 }
1423 }
1424 _ => panic!("Expected Pipeline"),
1425 }
1426 }
1427
1428 #[test]
1429 fn test_parse_three_command_pipeline() {
1430 let result = parse_line("ls -la | grep rush | wc -l").unwrap();
1431 match result {
1432 Statement::Complete(complete_cmd) => {
1433 if let CommandType::Pipeline(pipeline) = &complete_cmd.command {
1434 assert_eq!(pipeline.commands.len(), 3);
1435 } else {
1436 panic!("Expected Simple or Pipeline command");
1437 }
1438 }
1439 _ => panic!("Expected Pipeline"),
1440 }
1441 }
1442
1443 #[test]
1444 fn test_parse_input_redirect() {
1445 let result = parse_line("cat <file.txt").unwrap();
1446 match result {
1447 Statement::Complete(complete_cmd) => {
1448 if let CommandType::Simple(cmd) = &complete_cmd.command {
1449 assert_eq!(cmd.redirects.len(), 1);
1450 match &cmd.redirects[0] {
1451 Redirect::Input { file } => {
1452 assert!(file.is_literal());
1453 }
1454 _ => panic!("Expected Input redirect"),
1455 }
1456 } else {
1457 panic!("Expected Simple or Pipeline command");
1458 }
1459 }
1460 _ => panic!("Expected Simple command"),
1461 }
1462 }
1463
1464 #[test]
1465 fn test_parse_output_redirect() {
1466 let result = parse_line("echo hello >output.txt").unwrap();
1467 match result {
1468 Statement::Complete(complete_cmd) => {
1469 if let CommandType::Simple(cmd) = &complete_cmd.command {
1470 assert_eq!(cmd.redirects.len(), 1);
1471 match &cmd.redirects[0] {
1472 Redirect::Output { fd, file } => {
1473 assert_eq!(*fd, None);
1474 assert!(file.is_literal());
1475 }
1476 _ => panic!("Expected Output redirect"),
1477 }
1478 } else {
1479 panic!("Expected Simple or Pipeline command");
1480 }
1481 }
1482 _ => panic!("Expected Simple command"),
1483 }
1484 }
1485
1486 #[test]
1487 fn test_parse_append_redirect() {
1488 let result = parse_line("echo hello >>output.txt").unwrap();
1489 match result {
1490 Statement::Complete(complete_cmd) => {
1491 if let CommandType::Simple(cmd) = &complete_cmd.command {
1492 assert_eq!(cmd.redirects.len(), 1);
1493 match &cmd.redirects[0] {
1494 Redirect::OutputAppend { fd, file } => {
1495 assert_eq!(*fd, None);
1496 assert!(file.is_literal());
1497 }
1498 _ => panic!("Expected OutputAppend redirect"),
1499 }
1500 } else {
1501 panic!("Expected Simple or Pipeline command");
1502 }
1503 }
1504 _ => panic!("Expected Simple command"),
1505 }
1506 }
1507
1508 #[test]
1509 fn test_parse_stderr_redirect() {
1510 let result = parse_line("command 2>error.log").unwrap();
1511 match result {
1512 Statement::Complete(complete_cmd) => {
1513 if let CommandType::Simple(cmd) = &complete_cmd.command {
1514 assert_eq!(cmd.redirects.len(), 1);
1515 match &cmd.redirects[0] {
1516 Redirect::Output { fd, file } => {
1517 assert_eq!(*fd, Some(2));
1518 assert!(file.is_literal());
1519 }
1520 _ => panic!("Expected Output redirect with fd 2"),
1521 }
1522 } else {
1523 panic!("Expected Simple or Pipeline command");
1524 }
1525 }
1526 _ => panic!("Expected Simple command"),
1527 }
1528 }
1529
1530 #[test]
1531 fn test_parse_stderr_to_stdout() {
1532 let result = parse_line("command 2>&1").unwrap();
1533 match result {
1534 Statement::Complete(complete_cmd) => {
1535 if let CommandType::Simple(cmd) = &complete_cmd.command {
1536 assert_eq!(cmd.redirects.len(), 1);
1537 match &cmd.redirects[0] {
1538 Redirect::StderrToStdout => {}
1539 _ => panic!("Expected StderrToStdout redirect"),
1540 }
1541 } else {
1542 panic!("Expected Simple or Pipeline command");
1543 }
1544 }
1545 _ => panic!("Expected Simple command"),
1546 }
1547 }
1548
1549 #[test]
1550 fn test_parse_all_output_redirect() {
1551 let result = parse_line("command &>output.txt").unwrap();
1552 match result {
1553 Statement::Complete(complete_cmd) => {
1554 if let CommandType::Simple(cmd) = &complete_cmd.command {
1555 assert_eq!(cmd.redirects.len(), 1);
1556 match &cmd.redirects[0] {
1557 Redirect::AllOutput { file, append } => {
1558 assert!(!append);
1559 assert!(file.is_literal());
1560 }
1561 _ => panic!("Expected AllOutput redirect"),
1562 }
1563 } else {
1564 panic!("Expected Simple or Pipeline command");
1565 }
1566 }
1567 _ => panic!("Expected Simple command"),
1568 }
1569 }
1570
1571 #[test]
1572 fn test_parse_pipeline_with_redirects() {
1573 let result = parse_line("ls >list.txt | grep test").unwrap();
1574 match result {
1575 Statement::Complete(complete_cmd) => {
1576 if let CommandType::Pipeline(pipeline) = &complete_cmd.command {
1577 assert_eq!(pipeline.commands.len(), 2);
1578 if let PipelineElement::Simple(cmd) = &pipeline.commands[0] {
1579 assert_eq!(cmd.redirects.len(), 1);
1580 } else {
1581 panic!("Expected Simple command");
1582 }
1583 } else {
1584 panic!("Expected Simple or Pipeline command");
1585 }
1586 }
1587 _ => panic!("Expected Pipeline"),
1588 }
1589 }
1590
1591 #[test]
1592 fn test_parse_if_statement_newlines() {
1593 let input = "if test 5 -eq 5\nthen\necho equal\nfi";
1594 let result = parse_line(input);
1595 match result {
1596 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::If(_)) => {}
1597 Ok(other) => panic!("Expected If, got: {:?}", other),
1598 Err(e) => panic!("Parse error: {}", e),
1599 }
1600 }
1601
1602 #[test]
1603 fn test_parse_if_statement() {
1604 let input = "if test 5 -eq 5; then echo equal; fi";
1605 let result = parse_line(input);
1606 match result {
1607 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::If(_)) => {}
1608 Ok(other) => panic!("Expected If, got: {:?}", other),
1609 Err(e) => panic!("Parse error: {}", e),
1610 }
1611 }
1612
1613 #[test]
1614 fn test_parse_multiline_script() {
1615 let input = "echo hello\necho world\n";
1616 let result = parse_line(input);
1617 match result {
1618 Ok(Statement::Script(cmds)) => {
1619 assert_eq!(cmds.len(), 2);
1620 }
1621 Ok(other) => panic!("Expected Script, got: {:?}", other),
1622 Err(e) => panic!("Parse error: {}", e),
1623 }
1624 }
1625
1626 #[test]
1627 fn test_parse_while_loop() {
1628 let input = "while false; do echo test; done";
1629 let result = parse_line(input);
1630 match result {
1631 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::While(_)) => {}
1632 Ok(other) => panic!("Expected While, got: {:?}", other),
1633 Err(e) => panic!("Parse error: {}", e),
1634 }
1635 }
1636
1637 #[test]
1638 fn test_parse_while_loop_multiline() {
1639 let input = "while false\ndo\necho test\ndone";
1640 let result = parse_line(input);
1641 match result {
1642 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::While(_)) => {}
1643 Ok(other) => panic!("Expected While, got: {:?}", other),
1644 Err(e) => panic!("Parse error: {}", e),
1645 }
1646 }
1647
1648 #[test]
1649 fn test_parse_case_simple() {
1650 let input = "case x in a) echo matched;; esac";
1651 let result = parse_line(input);
1652 match result {
1653 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::Case(_)) => {}
1654 Ok(other) => panic!("Expected Case, got: {:?}", other),
1655 Err(e) => panic!("Parse error: {}", e),
1656 }
1657 }
1658
1659 #[test]
1660 fn test_parse_case_two_clauses() {
1661 let input = "case x in a) echo first;; b) echo second;; esac";
1662 let result = parse_line(input);
1663 match result {
1664 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::Case(_)) => {}
1665 Ok(other) => panic!("Expected Case, got: {:?}", other),
1666 Err(e) => panic!("Parse error: {}", e),
1667 }
1668 }
1669
1670 #[test]
1671 fn test_parse_background_execution() {
1672 let input = "sleep 10 &";
1673 let result = parse_line(input);
1674 match result {
1675 Ok(Statement::Complete(cmd)) => {
1676 assert!(cmd.background, "Command should be marked as background");
1677 assert!(matches!(cmd.command, CommandType::Simple(_)));
1678 }
1679 Ok(other) => panic!("Expected Complete, got: {:?}", other),
1680 Err(e) => panic!("Parse error: {}", e),
1681 }
1682 }
1683
1684 #[test]
1685 fn test_parse_foreground_execution() {
1686 let input = "sleep 10";
1687 let result = parse_line(input);
1688 match result {
1689 Ok(Statement::Complete(cmd)) => {
1690 assert!(!cmd.background, "Command should not be marked as background");
1691 }
1692 Ok(other) => panic!("Expected Complete, got: {:?}", other),
1693 Err(e) => panic!("Parse error: {}", e),
1694 }
1695 }
1696
1697 #[test]
1698 fn test_parse_select_statement() {
1699 let input = "select opt in a b c; do echo $opt; done";
1700 let result = parse_line(input);
1701 match result {
1702 Ok(Statement::Complete(cmd)) => {
1703 if let CommandType::Select(select) = &cmd.command {
1704 assert_eq!(select.var_name, "opt");
1705 assert_eq!(select.words.len(), 3);
1706 assert!(!select.body.is_empty());
1707 } else {
1708 panic!("Expected Select, got: {:?}", cmd.command);
1709 }
1710 }
1711 Ok(other) => panic!("Expected Complete, got: {:?}", other),
1712 Err(e) => panic!("Parse error: {}", e),
1713 }
1714 }
1715
1716 #[test]
1717 fn test_parse_select_multiline() {
1718 let input = "select item in foo bar baz\ndo\necho \"Selected: $item\"\ndone";
1719 let result = parse_line(input);
1720 match result {
1721 Ok(Statement::Complete(cmd)) if matches!(cmd.command, CommandType::Select(_)) => {}
1722 Ok(other) => panic!("Expected Select, got: {:?}", other),
1723 Err(e) => panic!("Parse error: {}", e),
1724 }
1725 }
1726 }
1727