shell_parser.rs

  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}