Text · 4748 bytes Raw Blame History
1 // Rush shell grammar (Phase 4 - Control Flow)
2
3 // Explicit whitespace handling (no automatic whitespace insertion)
4 ws = _{ " " | "\t" }
5 COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* }
6
7 // Top-level input
8 input = { SOI ~ ws* ~ command_line ~ ws* ~ EOI }
9
10 // Command line: can be compound commands or pipelines
11 command_line = { complete_command? }
12
13 // Complete command: can be a compound command or an and_or list
14 complete_command = {
15 if_statement
16 | while_statement
17 | for_statement
18 | case_statement
19 | and_or_list
20 }
21
22 // And/Or list: pipelines connected by && or ||
23 and_or_list = { pipeline ~ (ws* ~ and_or_op ~ ws* ~ pipeline)* }
24 and_or_op = { "&&" | "||" }
25
26 // Pipeline: commands connected by pipes
27 pipeline = { simple_command ~ (ws* ~ "|" ~ ws* ~ simple_command)* }
28
29 // Control flow statements
30 if_statement = {
31 "if" ~ separator ~ complete_command ~ separator ~ "then" ~ separator ~ command_list ~ separator
32 ~ elif_clause*
33 ~ else_clause?
34 ~ "fi"
35 }
36
37 elif_clause = {
38 "elif" ~ separator ~ complete_command ~ separator ~ "then" ~ separator ~ command_list ~ separator
39 }
40
41 else_clause = {
42 "else" ~ separator ~ command_list ~ separator
43 }
44
45 while_statement = {
46 "while" ~ separator ~ complete_command ~ separator ~ "do" ~ separator ~ command_list ~ separator ~ "done"
47 }
48
49 for_statement = {
50 "for" ~ ws+ ~ var_name ~ ws+ ~ "in" ~ (ws+ ~ word)* ~ separator ~ "do" ~ separator ~ command_list ~ separator ~ "done"
51 }
52
53 case_statement = {
54 "case" ~ ws+ ~ word ~ ws+ ~ "in" ~ separator ~ case_clause* ~ "esac"
55 }
56
57 case_clause = {
58 ws* ~ pattern ~ (ws* ~ "|" ~ ws* ~ pattern)* ~ ws* ~ ")" ~ separator ~ command_list ~ ws* ~ ";;" ~ separator
59 }
60
61 pattern = { word }
62
63 // Command list: one or more complete commands separated by newlines or semicolons
64 command_list = { (complete_command ~ separator)* ~ complete_command? }
65
66 // Separator: newlines, semicolons, or whitespace
67 separator = _{ (ws* ~ (NEWLINE | ";") ~ ws*)+ | ws+ }
68
69 // Simple command: assignments and/or words, with optional redirections
70 // Assignments must come before words (bash convention)
71 // Redirects can appear anywhere after words, and must be tried before words to capture fd numbers
72 simple_command = {
73 assignment ~ (ws+ ~ assignment)* ~ (ws+ ~ word ~ (ws+ ~ (redirect | word))*)? ~ (ws* ~ redirect)*
74 | word ~ (ws+ ~ (redirect | word))* ~ (ws* ~ redirect)*
75 | redirect ~ (ws* ~ redirect)*
76 }
77
78 // Variable assignment: NAME=value
79 assignment = { var_name ~ "=" ~ word }
80 var_name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
81
82 // Redirections: <file, >file, >>file, 2>file, 2>&1, &>file
83 redirect = {
84 redirect_stderr_to_stdout // Must come before others to match 2>&1
85 | redirect_all_output // &>file or &>>file
86 | redirect_output_append // >>file
87 | redirect_output // >file or N>file
88 | redirect_input // <file
89 }
90
91 redirect_input = { "<" ~ ws* ~ word }
92 redirect_output = { fd_number? ~ ">" ~ ws* ~ word }
93 redirect_output_append = { fd_number? ~ ">>" ~ ws* ~ word }
94 redirect_stderr_to_stdout = { "2>&1" }
95 redirect_all_output = { "&>" ~ ">"? ~ ws* ~ word } // &>file or &>>file
96
97 fd_number = @{ ASCII_DIGIT+ }
98
99 // Word: a sequence of word parts (can mix literal text, variables, command substitution)
100 // Parts are concatenated without whitespace (so "hello$VAR" is one word, "hello $VAR" is two)
101 word = { word_part+ }
102
103 word_part = {
104 quoted_string
105 | var_expansion
106 | command_substitution
107 | bare_word_part
108 }
109
110 // Variable expansion: $VAR or ${VAR} or ${VAR:-default}
111 var_expansion = {
112 "${" ~ var_name ~ var_modifier? ~ "}"
113 | "$" ~ var_name
114 }
115
116 var_modifier = {
117 ":-" ~ word // ${VAR:-default}
118 }
119
120 // Command substitution: $(command)
121 command_substitution = { "$(" ~ command_subst_content ~ ")" }
122 command_subst_content = @{ (!(")" | NEWLINE) ~ ANY)* }
123
124 // Bare word part: literal text (no special characters)
125 // Note: We exclude $, |, &, <, > to handle expansion, pipes, operators, and redirects separately
126 // "=" is allowed in words (needed for test command), assignments use explicit var_name pattern
127 bare_word_part = @{ (!(" " | "\t" | NEWLINE | "#" | "\"" | "'" | "$" | "|" | "&" | "<" | ">") ~ ANY)+ }
128
129 // Quoted strings (single quotes prevent expansion, double quotes allow it)
130 quoted_string = { double_quoted | single_quoted }
131
132 // Double quotes: allow variable expansion and command substitution inside
133 double_quoted = { "\"" ~ double_quoted_content ~ "\"" }
134 double_quoted_content = { double_quoted_part* }
135 double_quoted_part = {
136 var_expansion
137 | command_substitution
138 | double_quoted_text
139 }
140 double_quoted_text = @{ (!"\"" ~ !"$" ~ ANY)+ }
141
142 // Single quotes: no expansion (literal)
143 single_quoted = @{ "'" ~ (!"'" ~ ANY)* ~ "'" }
144
145 NEWLINE = { "\n" | "\r\n" }