1use brush_parser::ast;
2use brush_parser::word::WordPiece;
3use brush_parser::{Parser, ParserOptions, SourceInfo};
4use std::io::BufReader;
5
6pub fn extract_commands(command: &str) -> Option<Vec<String>> {
7 let reader = BufReader::new(command.as_bytes());
8 let options = ParserOptions::default();
9 let source_info = SourceInfo::default();
10 let mut parser = Parser::new(reader, &options, &source_info);
11
12 let program = parser.parse_program().ok()?;
13
14 let mut commands = Vec::new();
15 extract_commands_from_program(&program, &mut commands);
16
17 Some(commands)
18}
19
20fn extract_commands_from_program(program: &ast::Program, commands: &mut Vec<String>) {
21 for complete_command in &program.complete_commands {
22 extract_commands_from_compound_list(complete_command, commands);
23 }
24}
25
26fn extract_commands_from_compound_list(
27 compound_list: &ast::CompoundList,
28 commands: &mut Vec<String>,
29) {
30 for item in &compound_list.0 {
31 extract_commands_from_and_or_list(&item.0, commands);
32 }
33}
34
35fn extract_commands_from_and_or_list(and_or_list: &ast::AndOrList, commands: &mut Vec<String>) {
36 extract_commands_from_pipeline(&and_or_list.first, commands);
37
38 for and_or in &and_or_list.additional {
39 match and_or {
40 ast::AndOr::And(pipeline) | ast::AndOr::Or(pipeline) => {
41 extract_commands_from_pipeline(pipeline, commands);
42 }
43 }
44 }
45}
46
47fn extract_commands_from_pipeline(pipeline: &ast::Pipeline, commands: &mut Vec<String>) {
48 for command in &pipeline.seq {
49 extract_commands_from_command(command, commands);
50 }
51}
52
53fn extract_commands_from_command(command: &ast::Command, commands: &mut Vec<String>) {
54 match command {
55 ast::Command::Simple(simple_command) => {
56 extract_commands_from_simple_command(simple_command, commands);
57 }
58 ast::Command::Compound(compound_command, _redirect_list) => {
59 extract_commands_from_compound_command(compound_command, commands);
60 }
61 ast::Command::Function(func_def) => {
62 extract_commands_from_function_body(&func_def.body, commands);
63 }
64 ast::Command::ExtendedTest(test_expr) => {
65 extract_commands_from_extended_test_expr(test_expr, commands);
66 }
67 }
68}
69
70fn extract_commands_from_simple_command(
71 simple_command: &ast::SimpleCommand,
72 commands: &mut Vec<String>,
73) {
74 let command_str = simple_command.to_string();
75 if !command_str.trim().is_empty() {
76 commands.push(command_str);
77 }
78
79 if let Some(prefix) = &simple_command.prefix {
80 extract_commands_from_command_prefix(prefix, commands);
81 }
82 if let Some(word) = &simple_command.word_or_name {
83 extract_commands_from_word(word, commands);
84 }
85 if let Some(suffix) = &simple_command.suffix {
86 extract_commands_from_command_suffix(suffix, commands);
87 }
88}
89
90fn extract_commands_from_command_prefix(prefix: &ast::CommandPrefix, commands: &mut Vec<String>) {
91 for item in &prefix.0 {
92 extract_commands_from_prefix_or_suffix_item(item, commands);
93 }
94}
95
96fn extract_commands_from_command_suffix(suffix: &ast::CommandSuffix, commands: &mut Vec<String>) {
97 for item in &suffix.0 {
98 extract_commands_from_prefix_or_suffix_item(item, commands);
99 }
100}
101
102fn extract_commands_from_prefix_or_suffix_item(
103 item: &ast::CommandPrefixOrSuffixItem,
104 commands: &mut Vec<String>,
105) {
106 match item {
107 ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) => {
108 extract_commands_from_io_redirect(redirect, commands);
109 }
110 ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => {
111 extract_commands_from_assignment(assignment, commands);
112 }
113 ast::CommandPrefixOrSuffixItem::Word(word) => {
114 extract_commands_from_word(word, commands);
115 }
116 ast::CommandPrefixOrSuffixItem::ProcessSubstitution(_kind, subshell) => {
117 extract_commands_from_compound_list(&subshell.list, commands);
118 }
119 }
120}
121
122fn extract_commands_from_io_redirect(redirect: &ast::IoRedirect, commands: &mut Vec<String>) {
123 match redirect {
124 ast::IoRedirect::File(_fd, _kind, target) => {
125 if let ast::IoFileRedirectTarget::ProcessSubstitution(_kind, subshell) = target {
126 extract_commands_from_compound_list(&subshell.list, commands);
127 }
128 }
129 ast::IoRedirect::HereDocument(_fd, _here_doc) => {}
130 ast::IoRedirect::HereString(_fd, word) => {
131 extract_commands_from_word(word, commands);
132 }
133 ast::IoRedirect::OutputAndError(word, _) => {
134 extract_commands_from_word(word, commands);
135 }
136 }
137}
138
139fn extract_commands_from_assignment(assignment: &ast::Assignment, commands: &mut Vec<String>) {
140 match &assignment.value {
141 ast::AssignmentValue::Scalar(word) => {
142 extract_commands_from_word(word, commands);
143 }
144 ast::AssignmentValue::Array(words) => {
145 for (opt_word, word) in words {
146 if let Some(w) = opt_word {
147 extract_commands_from_word(w, commands);
148 }
149 extract_commands_from_word(word, commands);
150 }
151 }
152 }
153}
154
155fn extract_commands_from_word(word: &ast::Word, commands: &mut Vec<String>) {
156 let options = ParserOptions::default();
157 if let Ok(pieces) = brush_parser::word::parse(&word.value, &options) {
158 for piece_with_source in pieces {
159 extract_commands_from_word_piece(&piece_with_source.piece, commands);
160 }
161 }
162}
163
164fn extract_commands_from_word_piece(piece: &WordPiece, commands: &mut Vec<String>) {
165 match piece {
166 WordPiece::CommandSubstitution(cmd_str)
167 | WordPiece::BackquotedCommandSubstitution(cmd_str) => {
168 if let Some(nested_commands) = extract_commands(cmd_str) {
169 commands.extend(nested_commands);
170 }
171 }
172 WordPiece::DoubleQuotedSequence(pieces)
173 | WordPiece::GettextDoubleQuotedSequence(pieces) => {
174 for inner_piece_with_source in pieces {
175 extract_commands_from_word_piece(&inner_piece_with_source.piece, commands);
176 }
177 }
178 WordPiece::EscapeSequence(_)
179 | WordPiece::SingleQuotedText(_)
180 | WordPiece::Text(_)
181 | WordPiece::AnsiCQuotedText(_)
182 | WordPiece::TildePrefix(_)
183 | WordPiece::ParameterExpansion(_)
184 | WordPiece::ArithmeticExpression(_) => {}
185 }
186}
187
188fn extract_commands_from_compound_command(
189 compound_command: &ast::CompoundCommand,
190 commands: &mut Vec<String>,
191) {
192 match compound_command {
193 ast::CompoundCommand::BraceGroup(brace_group) => {
194 extract_commands_from_compound_list(&brace_group.list, commands);
195 }
196 ast::CompoundCommand::Subshell(subshell) => {
197 extract_commands_from_compound_list(&subshell.list, commands);
198 }
199 ast::CompoundCommand::ForClause(for_clause) => {
200 if let Some(words) = &for_clause.values {
201 for word in words {
202 extract_commands_from_word(word, commands);
203 }
204 }
205 extract_commands_from_do_group(&for_clause.body, commands);
206 }
207 ast::CompoundCommand::CaseClause(case_clause) => {
208 extract_commands_from_word(&case_clause.value, commands);
209 for item in &case_clause.cases {
210 if let Some(body) = &item.cmd {
211 extract_commands_from_compound_list(body, commands);
212 }
213 }
214 }
215 ast::CompoundCommand::IfClause(if_clause) => {
216 extract_commands_from_compound_list(&if_clause.condition, commands);
217 extract_commands_from_compound_list(&if_clause.then, commands);
218 if let Some(elses) = &if_clause.elses {
219 for else_item in elses {
220 if let Some(condition) = &else_item.condition {
221 extract_commands_from_compound_list(condition, commands);
222 }
223 extract_commands_from_compound_list(&else_item.body, commands);
224 }
225 }
226 }
227 ast::CompoundCommand::WhileClause(while_clause)
228 | ast::CompoundCommand::UntilClause(while_clause) => {
229 extract_commands_from_compound_list(&while_clause.0, commands);
230 extract_commands_from_do_group(&while_clause.1, commands);
231 }
232 ast::CompoundCommand::ArithmeticForClause(arith_for) => {
233 extract_commands_from_do_group(&arith_for.body, commands);
234 }
235 ast::CompoundCommand::Arithmetic(_arith_cmd) => {}
236 }
237}
238
239fn extract_commands_from_do_group(do_group: &ast::DoGroupCommand, commands: &mut Vec<String>) {
240 extract_commands_from_compound_list(&do_group.list, commands);
241}
242
243fn extract_commands_from_function_body(func_body: &ast::FunctionBody, commands: &mut Vec<String>) {
244 extract_commands_from_compound_command(&func_body.0, commands);
245}
246
247fn extract_commands_from_extended_test_expr(
248 test_expr: &ast::ExtendedTestExprCommand,
249 commands: &mut Vec<String>,
250) {
251 extract_commands_from_extended_test_expr_inner(&test_expr.expr, commands);
252}
253
254fn extract_commands_from_extended_test_expr_inner(
255 expr: &ast::ExtendedTestExpr,
256 commands: &mut Vec<String>,
257) {
258 match expr {
259 ast::ExtendedTestExpr::Not(inner) => {
260 extract_commands_from_extended_test_expr_inner(inner, commands);
261 }
262 ast::ExtendedTestExpr::And(left, right) | ast::ExtendedTestExpr::Or(left, right) => {
263 extract_commands_from_extended_test_expr_inner(left, commands);
264 extract_commands_from_extended_test_expr_inner(right, commands);
265 }
266 ast::ExtendedTestExpr::Parenthesized(inner) => {
267 extract_commands_from_extended_test_expr_inner(inner, commands);
268 }
269 ast::ExtendedTestExpr::UnaryTest(_, word) => {
270 extract_commands_from_word(word, commands);
271 }
272 ast::ExtendedTestExpr::BinaryTest(_, word1, word2) => {
273 extract_commands_from_word(word1, commands);
274 extract_commands_from_word(word2, commands);
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_simple_command() {
285 let commands = extract_commands("ls").expect("parse failed");
286 assert_eq!(commands, vec!["ls"]);
287 }
288
289 #[test]
290 fn test_command_with_args() {
291 let commands = extract_commands("ls -la /tmp").expect("parse failed");
292 assert_eq!(commands, vec!["ls -la /tmp"]);
293 }
294
295 #[test]
296 fn test_and_operator() {
297 let commands = extract_commands("ls && rm -rf /").expect("parse failed");
298 assert_eq!(commands, vec!["ls", "rm -rf /"]);
299 }
300
301 #[test]
302 fn test_or_operator() {
303 let commands = extract_commands("ls || rm -rf /").expect("parse failed");
304 assert_eq!(commands, vec!["ls", "rm -rf /"]);
305 }
306
307 #[test]
308 fn test_semicolon() {
309 let commands = extract_commands("ls; rm -rf /").expect("parse failed");
310 assert_eq!(commands, vec!["ls", "rm -rf /"]);
311 }
312
313 #[test]
314 fn test_pipe() {
315 let commands = extract_commands("ls | xargs rm -rf").expect("parse failed");
316 assert_eq!(commands, vec!["ls", "xargs rm -rf"]);
317 }
318
319 #[test]
320 fn test_background() {
321 let commands = extract_commands("ls & rm -rf /").expect("parse failed");
322 assert_eq!(commands, vec!["ls", "rm -rf /"]);
323 }
324
325 #[test]
326 fn test_command_substitution_dollar() {
327 let commands = extract_commands("echo $(whoami)").expect("parse failed");
328 assert!(commands.iter().any(|c| c.contains("echo")));
329 assert!(commands.contains(&"whoami".to_string()));
330 }
331
332 #[test]
333 fn test_command_substitution_backticks() {
334 let commands = extract_commands("echo `whoami`").expect("parse failed");
335 assert!(commands.iter().any(|c| c.contains("echo")));
336 assert!(commands.contains(&"whoami".to_string()));
337 }
338
339 #[test]
340 fn test_process_substitution_input() {
341 let commands = extract_commands("cat <(ls)").expect("parse failed");
342 assert!(commands.iter().any(|c| c.contains("cat")));
343 assert!(commands.contains(&"ls".to_string()));
344 }
345
346 #[test]
347 fn test_process_substitution_output() {
348 let commands = extract_commands("ls >(cat)").expect("parse failed");
349 assert!(commands.iter().any(|c| c.contains("ls")));
350 assert!(commands.contains(&"cat".to_string()));
351 }
352
353 #[test]
354 fn test_newline_separator() {
355 let commands = extract_commands("ls\nrm -rf /").expect("parse failed");
356 assert_eq!(commands, vec!["ls", "rm -rf /"]);
357 }
358
359 #[test]
360 fn test_subshell() {
361 let commands = extract_commands("(ls && rm -rf /)").expect("parse failed");
362 assert_eq!(commands, vec!["ls", "rm -rf /"]);
363 }
364
365 #[test]
366 fn test_mixed_operators() {
367 let commands = extract_commands("ls; echo hello && rm -rf /").expect("parse failed");
368 assert_eq!(commands, vec!["ls", "echo hello", "rm -rf /"]);
369 }
370
371 #[test]
372 fn test_no_spaces_around_operators() {
373 let commands = extract_commands("ls&&rm").expect("parse failed");
374 assert_eq!(commands, vec!["ls", "rm"]);
375 }
376
377 #[test]
378 fn test_nested_command_substitution() {
379 let commands = extract_commands("echo $(cat $(whoami).txt)").expect("parse failed");
380 assert!(commands.iter().any(|c| c.contains("echo")));
381 assert!(commands.iter().any(|c| c.contains("cat")));
382 assert!(commands.contains(&"whoami".to_string()));
383 }
384
385 #[test]
386 fn test_empty_command() {
387 let commands = extract_commands("").expect("parse failed");
388 assert!(commands.is_empty());
389 }
390
391 #[test]
392 fn test_invalid_syntax_returns_none() {
393 let result = extract_commands("ls &&");
394 assert!(result.is_none());
395 }
396}