1use crate::parser::ast::{Command, Op, Pipeline, Program, Redirection};
2use crate::parser::chain::parse_builder_chain;
3use crate::parser::errors::ParseError;
4use crate::parser::lexer::{lex, Token};
5use crate::parser::normalize::normalize_name;
6use crate::parser::normalize::parse_test_id;
7use crate::parser::priority::infer_priority;
8use crate::parser::suffixes::{strip_optional, strip_urgency, strip_verbosity};
9
10#[must_use = "parsing produces a Program AST; ignoring it is likely a bug"]
61pub fn parse(input: &str) -> Result<Program, ParseError> {
62 let tokens = lex(input)?;
63 let mut pipelines: Vec<Pipeline> = Vec::new();
64 let mut commands: Vec<Command> = Vec::new();
65 let mut after_redirection = false;
66
67 let mut i = 0;
68 while i < tokens.len() {
69 let token = &tokens[i];
70
71 if after_redirection && !matches!(token, Token::And | Token::Or) {
72 return Err(ParseError::InvalidRedirection {
73 token: token_str(token),
74 position: i,
75 });
76 }
77
78 match token {
79 Token::And | Token::Or => {
80 if commands.is_empty() {
81 return Err(ParseError::MissingOperator {
82 token: token_str(token),
83 position: i,
84 });
85 }
86 let op = match token {
87 Token::And => Op::And,
88 Token::Or => Op::Or,
89 _ => unreachable!(),
90 };
91 pipelines.push(Pipeline {
92 commands,
93 operator: Some(op),
94 });
95 commands = Vec::new();
96 after_redirection = false;
97 i += 1;
98 }
99
100 Token::Pipe | Token::PipeErr => {
101 if commands.is_empty() {
102 return Err(ParseError::MissingOperator {
103 token: token_str(token),
104 position: i,
105 });
106 }
107 let op = match token {
108 Token::Pipe => Op::Pipe,
109 Token::PipeErr => Op::PipeErr,
110 _ => unreachable!(),
111 };
112 if let Some(last) = commands.last_mut() {
113 if last.pipe.is_some() {
114 return Err(ParseError::MissingOperator {
115 token: token_str(token),
116 position: i,
117 });
118 }
119 last.pipe = Some(op);
120 }
121 i += 1;
122 }
123
124 Token::Redirect | Token::Append => {
125 if commands.is_empty() {
126 return Err(ParseError::InvalidRedirection {
127 token: token_str(token),
128 position: i,
129 });
130 }
131 if i + 1 >= tokens.len() {
132 return Err(ParseError::InvalidRedirection {
133 token: token_str(token),
134 position: i,
135 });
136 }
137 let target_token = &tokens[i + 1];
138 if matches!(
139 target_token,
140 Token::And
141 | Token::Or
142 | Token::Pipe
143 | Token::PipeErr
144 | Token::Redirect
145 | Token::Append
146 ) {
147 return Err(ParseError::InvalidRedirection {
148 token: token_str(token),
149 position: i,
150 });
151 }
152 let target = match target_token {
153 Token::Word(w) => w.clone(),
154 Token::Command(c) => c.clone(),
155 _ => {
156 return Err(ParseError::InvalidRedirection {
157 token: token_str(token),
158 position: i,
159 })
160 }
161 };
162 let redirect = if matches!(token, Token::Redirect) {
163 Redirection::Truncate(target)
164 } else {
165 Redirection::Append(target)
166 };
167 if let Some(last) = commands.last_mut() {
168 last.redirect = Some(redirect);
169 }
170 after_redirection = true;
171 i += 2;
172 }
173
174 Token::Word(w) => {
175 return Err(if w.starts_with('!') {
176 ParseError::InvalidBang {
177 token: w.clone(),
178 position: i,
179 }
180 } else {
181 ParseError::MissingOperator {
182 token: w.clone(),
183 position: i,
184 }
185 });
186 }
187
188 Token::Command(raw) => {
189 if let Some(last) = commands.last() {
190 if last.pipe.is_none() {
191 return Err(ParseError::MissingOperator {
192 token: raw.clone(),
193 position: i,
194 });
195 }
196 }
197
198 let (after_urgency, urgency) =
200 strip_urgency(raw).ok_or_else(|| ParseError::InvalidBang {
201 token: raw.clone(),
202 position: i,
203 })?;
204 let (after_verbosity, verbosity) = strip_verbosity(after_urgency);
205 let (bare_token, optional) =
206 strip_optional(after_verbosity).map_err(|_| ParseError::InvalidSuffix {
207 token: raw.clone(),
208 position: i,
209 })?;
210
211 let parts =
213 parse_builder_chain(bare_token).map_err(|_| ParseError::MalformedChain {
214 token: raw.clone(),
215 position: i,
216 })?;
217 let (cmd_raw, args) = (parts.name, parts.args);
218
219 if cmd_raw.chars().any(|c| c.is_ascii_digit()) && parse_test_id(&cmd_raw).is_none()
220 {
221 return Err(ParseError::InvalidDigits {
222 token: raw.clone(),
223 position: i,
224 });
225 }
226
227 let name = normalize_name(&cmd_raw);
228 let test_id = parse_test_id(&cmd_raw);
229 let mut cmd = Command::new(name, infer_priority(&cmd_raw));
230 cmd.raw = raw.to_string();
231 cmd.urgency = urgency;
232 cmd.verbosity = verbosity;
233 cmd.optional = optional;
234 cmd.primary = parts.primary;
235 cmd.args = args;
236 cmd.test_id = test_id;
237 commands.push(cmd);
238 after_redirection = false;
239 i += 1;
240 }
241 }
242 }
243
244 if !commands.is_empty() {
245 pipelines.push(Pipeline {
246 commands,
247 operator: None,
248 });
249 }
250
251 Ok(Program { pipelines })
252}
253
254pub mod ast;
255pub mod chain;
256pub mod errors;
257pub mod lexer;
258pub mod normalize;
259pub mod priority;
260pub mod suffixes;
261
262fn token_str(token: &Token) -> String {
263 match token {
264 Token::Command(s) | Token::Word(s) => s.clone(),
265 Token::Pipe => "|".to_string(),
266 Token::PipeErr => "|&".to_string(),
267 Token::And => "&&".to_string(),
268 Token::Or => "||".to_string(),
269 Token::Redirect => ">".to_string(),
270 Token::Append => ">>".to_string(),
271 }
272}