use brush_parser::ast;use brush_parser::word::WordPiece;use brush_parser::{Parser, ParserOptions, SourceInfo};use std::io::BufReader; pub fn extract_commands(command: &str) -> Option<Vec<String>> { let reader = BufReader::new(command.as_bytes()); let options = ParserOptions::default(); let source_info = SourceInfo::default(); let mut parser = Parser::new(reader, &options, &source_info); let program = parser.parse_program().ok()?; let mut commands = Vec::new(); extract_commands_from_program(&program, &mut commands)?; Some(commands)} fn extract_commands_from_program(program: &ast::Program, commands: &mut Vec<String>) -> Option<()> { for complete_command in &program.complete_commands { extract_commands_from_compound_list(complete_command, commands)?; } Some(())} fn extract_commands_from_compound_list( compound_list: &ast::CompoundList, commands: &mut Vec<String>,) -> Option<()> { for item in &compound_list.0 { extract_commands_from_and_or_list(&item.0, commands)?; } Some(())} fn extract_commands_from_and_or_list( and_or_list: &ast::AndOrList, commands: &mut Vec<String>,) -> Option<()> { extract_commands_from_pipeline(&and_or_list.first, commands)?; for and_or in &and_or_list.additional { match and_or { ast::AndOr::And(pipeline) | ast::AndOr::Or(pipeline) => { extract_commands_from_pipeline(pipeline, commands)?; } } } Some(())} fn extract_commands_from_pipeline( pipeline: &ast::Pipeline, commands: &mut Vec<String>,) -> Option<()> { for command in &pipeline.seq { extract_commands_from_command(command, commands)?; } Some(())} fn extract_commands_from_command(command: &ast::Command, commands: &mut Vec<String>) -> Option<()> { match command { ast::Command::Simple(simple_command) => { extract_commands_from_simple_command(simple_command, commands)?; } ast::Command::Compound(compound_command, redirect_list) => { let body_start = extract_commands_from_compound_command(compound_command, commands)?; if let Some(redirect_list) = redirect_list { let mut normalized_redirects = Vec::new(); for redirect in &redirect_list.0 { match normalize_io_redirect(redirect)? { RedirectNormalization::Normalized(s) => normalized_redirects.push(s), RedirectNormalization::Skip => {} } } if !normalized_redirects.is_empty() { if body_start >= commands.len() { return None; } commands.extend(normalized_redirects); } for redirect in &redirect_list.0 { extract_commands_from_io_redirect(redirect, commands)?; } } } ast::Command::Function(func_def) => { extract_commands_from_function_body(&func_def.body, commands)?; } ast::Command::ExtendedTest(test_expr) => { extract_commands_from_extended_test_expr(test_expr, commands)?; } } Some(())} enum RedirectNormalization { Normalized(String), Skip,} fn extract_commands_from_simple_command( simple_command: &ast::SimpleCommand, commands: &mut Vec<String>,) -> Option<()> { // Build a normalized command string from individual words, stripping shell // quotes so that security patterns match regardless of quoting style. // For example, both `rm -rf '/'` and `rm -rf /` normalize to "rm -rf /". // // If any word fails to normalize, we return None so that `extract_commands` // returns None — the same as a shell parse failure. The caller then falls // back to raw-input matching with always_allow disabled. let mut words = Vec::new(); let mut redirects = Vec::new(); if let Some(prefix) = &simple_command.prefix { for item in &prefix.0 { if let ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) = item { match normalize_io_redirect(redirect) { Some(RedirectNormalization::Normalized(s)) => redirects.push(s), Some(RedirectNormalization::Skip) => {} None => return None, } } } } if let Some(word) = &simple_command.word_or_name { words.push(normalize_word(word)?); } if let Some(suffix) = &simple_command.suffix { for item in &suffix.0 { match item { ast::CommandPrefixOrSuffixItem::Word(word) => { words.push(normalize_word(word)?); } ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) => { match normalize_io_redirect(redirect) { Some(RedirectNormalization::Normalized(s)) => redirects.push(s), Some(RedirectNormalization::Skip) => {} None => return None, } } _ => {} } } } if words.is_empty() && !redirects.is_empty() { return None; } let command_str = words.join(" "); if !command_str.is_empty() { commands.push(command_str); } commands.extend(redirects); // Extract nested commands from command substitutions, process substitutions, etc. if let Some(prefix) = &simple_command.prefix { extract_commands_from_command_prefix(prefix, commands)?; } if let Some(word) = &simple_command.word_or_name { extract_commands_from_word(word, commands)?; } if let Some(suffix) = &simple_command.suffix { extract_commands_from_command_suffix(suffix, commands)?; } Some(())} /// Normalizes a shell word by stripping quoting syntax and returning the/// semantic (unquoted) value. Returns `None` if word parsing fails.fn normalize_word(word: &ast::Word) -> Option<String> { let options = ParserOptions::default(); let pieces = brush_parser::word::parse(&word.value, &options).ok()?; let mut result = String::new(); for piece_with_source in &pieces { normalize_word_piece_into( &piece_with_source.piece, &word.value, piece_with_source.start_index, piece_with_source.end_index, &mut result, )?; } Some(result)} fn normalize_word_piece_into( piece: &WordPiece, raw_value: &str, start_index: usize, end_index: usize, result: &mut String,) -> Option<()> { match piece { WordPiece::Text(text) => result.push_str(text), WordPiece::SingleQuotedText(text) => result.push_str(text), WordPiece::AnsiCQuotedText(text) => result.push_str(text), WordPiece::EscapeSequence(text) => { result.push_str(text.strip_prefix('\\').unwrap_or(text)); } WordPiece::DoubleQuotedSequence(pieces) | WordPiece::GettextDoubleQuotedSequence(pieces) => { for inner in pieces { normalize_word_piece_into( &inner.piece, raw_value, inner.start_index, inner.end_index, result, )?; } } WordPiece::TildePrefix(prefix) => { result.push('~'); result.push_str(prefix); } // For parameter expansions, command substitutions, and arithmetic expressions, // preserve the original source text so that patterns like `\$HOME` continue // to match. WordPiece::ParameterExpansion(_) | WordPiece::CommandSubstitution(_) | WordPiece::BackquotedCommandSubstitution(_) | WordPiece::ArithmeticExpression(_) => { let source = raw_value.get(start_index..end_index)?; result.push_str(source); } } Some(())} fn normalize_io_redirect(redirect: &ast::IoRedirect) -> Option<RedirectNormalization> { match redirect { ast::IoRedirect::File(fd, kind, target) => { let target_word = match target { ast::IoFileRedirectTarget::Filename(word) => word, _ => return Some(RedirectNormalization::Skip), }; let operator = match kind { ast::IoFileRedirectKind::Read => "<", ast::IoFileRedirectKind::Write => ">", ast::IoFileRedirectKind::Append => ">>", ast::IoFileRedirectKind::ReadAndWrite => "<>", ast::IoFileRedirectKind::Clobber => ">|", // The parser pairs DuplicateInput/DuplicateOutput with // IoFileRedirectTarget::Duplicate (not Filename), so the // target match above will return Skip before we reach here. // These arms are kept for defensiveness. ast::IoFileRedirectKind::DuplicateInput => "<&", ast::IoFileRedirectKind::DuplicateOutput => ">&", }; let fd_prefix = match fd { Some(fd) => fd.to_string(), None => String::new(), }; let normalized = normalize_word(target_word)?; Some(RedirectNormalization::Normalized(format!( "{}{} {}", fd_prefix, operator, normalized ))) } ast::IoRedirect::OutputAndError(word, append) => { let operator = if *append { "&>>" } else { "&>" }; let normalized = normalize_word(word)?; Some(RedirectNormalization::Normalized(format!( "{} {}", operator, normalized ))) } ast::IoRedirect::HereDocument(_, _) | ast::IoRedirect::HereString(_, _) => { Some(RedirectNormalization::Skip) } }} fn extract_commands_from_command_prefix( prefix: &ast::CommandPrefix, commands: &mut Vec<String>,) -> Option<()> { for item in &prefix.0 { extract_commands_from_prefix_or_suffix_item(item, commands)?; } Some(())} fn extract_commands_from_command_suffix( suffix: &ast::CommandSuffix, commands: &mut Vec<String>,) -> Option<()> { for item in &suffix.0 { extract_commands_from_prefix_or_suffix_item(item, commands)?; } Some(())} fn extract_commands_from_prefix_or_suffix_item( item: &ast::CommandPrefixOrSuffixItem, commands: &mut Vec<String>,) -> Option<()> { match item { ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) => { extract_commands_from_io_redirect(redirect, commands)?; } ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => { extract_commands_from_assignment(assignment, commands)?; } ast::CommandPrefixOrSuffixItem::Word(word) => { extract_commands_from_word(word, commands)?; } ast::CommandPrefixOrSuffixItem::ProcessSubstitution(_kind, subshell) => { extract_commands_from_compound_list(&subshell.list, commands)?; } } Some(())} fn extract_commands_from_io_redirect( redirect: &ast::IoRedirect, commands: &mut Vec<String>,) -> Option<()> { match redirect { ast::IoRedirect::File(_fd, _kind, target) => match target { ast::IoFileRedirectTarget::ProcessSubstitution(_kind, subshell) => { extract_commands_from_compound_list(&subshell.list, commands)?; } ast::IoFileRedirectTarget::Filename(word) => { extract_commands_from_word(word, commands)?; } _ => {} }, ast::IoRedirect::HereDocument(_fd, here_doc) => { if here_doc.requires_expansion { extract_commands_from_word(&here_doc.doc, commands)?; } } ast::IoRedirect::HereString(_fd, word) => { extract_commands_from_word(word, commands)?; } ast::IoRedirect::OutputAndError(word, _) => { extract_commands_from_word(word, commands)?; } } Some(())} fn extract_commands_from_assignment( assignment: &ast::Assignment, commands: &mut Vec<String>,) -> Option<()> { match &assignment.value { ast::AssignmentValue::Scalar(word) => { extract_commands_from_word(word, commands)?; } ast::AssignmentValue::Array(words) => { for (opt_word, word) in words { if let Some(w) = opt_word { extract_commands_from_word(w, commands)?; } extract_commands_from_word(word, commands)?; } } } Some(())} fn extract_commands_from_word(word: &ast::Word, commands: &mut Vec<String>) -> Option<()> { let options = ParserOptions::default(); let pieces = brush_parser::word::parse(&word.value, &options).ok()?; for piece_with_source in pieces { extract_commands_from_word_piece(&piece_with_source.piece, commands)?; } Some(())} fn extract_commands_from_word_piece(piece: &WordPiece, commands: &mut Vec<String>) -> Option<()> { match piece { WordPiece::CommandSubstitution(cmd_str) | WordPiece::BackquotedCommandSubstitution(cmd_str) => { let nested_commands = extract_commands(cmd_str)?; commands.extend(nested_commands); } WordPiece::DoubleQuotedSequence(pieces) | WordPiece::GettextDoubleQuotedSequence(pieces) => { for inner_piece_with_source in pieces { extract_commands_from_word_piece(&inner_piece_with_source.piece, commands)?; } } WordPiece::EscapeSequence(_) | WordPiece::SingleQuotedText(_) | WordPiece::Text(_) | WordPiece::AnsiCQuotedText(_) | WordPiece::TildePrefix(_) | WordPiece::ParameterExpansion(_) | WordPiece::ArithmeticExpression(_) => {} } Some(())} fn extract_commands_from_compound_command( compound_command: &ast::CompoundCommand, commands: &mut Vec<String>,) -> Option<usize> { match compound_command { ast::CompoundCommand::BraceGroup(brace_group) => { let body_start = commands.len(); extract_commands_from_compound_list(&brace_group.list, commands)?; Some(body_start) } ast::CompoundCommand::Subshell(subshell) => { let body_start = commands.len(); extract_commands_from_compound_list(&subshell.list, commands)?; Some(body_start) } ast::CompoundCommand::ForClause(for_clause) => { if let Some(words) = &for_clause.values { for word in words { extract_commands_from_word(word, commands)?; } } let body_start = commands.len(); extract_commands_from_do_group(&for_clause.body, commands)?; Some(body_start) } ast::CompoundCommand::CaseClause(case_clause) => { extract_commands_from_word(&case_clause.value, commands)?; let body_start = commands.len(); for item in &case_clause.cases { if let Some(body) = &item.cmd { extract_commands_from_compound_list(body, commands)?; } } Some(body_start) } ast::CompoundCommand::IfClause(if_clause) => { extract_commands_from_compound_list(&if_clause.condition, commands)?; let body_start = commands.len(); extract_commands_from_compound_list(&if_clause.then, commands)?; if let Some(elses) = &if_clause.elses { for else_item in elses { if let Some(condition) = &else_item.condition { extract_commands_from_compound_list(condition, commands)?; } extract_commands_from_compound_list(&else_item.body, commands)?; } } Some(body_start) } ast::CompoundCommand::WhileClause(while_clause) | ast::CompoundCommand::UntilClause(while_clause) => { extract_commands_from_compound_list(&while_clause.0, commands)?; let body_start = commands.len(); extract_commands_from_do_group(&while_clause.1, commands)?; Some(body_start) } ast::CompoundCommand::ArithmeticForClause(arith_for) => { let body_start = commands.len(); extract_commands_from_do_group(&arith_for.body, commands)?; Some(body_start) } ast::CompoundCommand::Arithmetic(_arith_cmd) => Some(commands.len()), }} fn extract_commands_from_do_group( do_group: &ast::DoGroupCommand, commands: &mut Vec<String>,) -> Option<()> { extract_commands_from_compound_list(&do_group.list, commands)} fn extract_commands_from_function_body( func_body: &ast::FunctionBody, commands: &mut Vec<String>,) -> Option<()> { let body_start = extract_commands_from_compound_command(&func_body.0, commands)?; if let Some(redirect_list) = &func_body.1 { let mut normalized_redirects = Vec::new(); for redirect in &redirect_list.0 { match normalize_io_redirect(redirect)? { RedirectNormalization::Normalized(s) => normalized_redirects.push(s), RedirectNormalization::Skip => {} } } if !normalized_redirects.is_empty() { if body_start >= commands.len() { return None; } commands.extend(normalized_redirects); } for redirect in &redirect_list.0 { extract_commands_from_io_redirect(redirect, commands)?; } } Some(())} fn extract_commands_from_extended_test_expr(Lines 1–500 of 993. Load more
use brush_parser::ast; use brush_parser::word::WordPiece; use brush_parser::{Parser, ParserOptions, SourceInfo}; use std::io::BufReader; pub fn extract_commands(command: &str) -> Option<Vec<String>> { let reader = BufReader::new(command.as_bytes()); let options = ParserOptions::default(); let source_info = SourceInfo::default(); let mut parser = Parser::new(reader, &options, &source_info); let program = parser.parse_program().ok()?; let mut commands = Vec::new(); extract_commands_from_program(&program, &mut commands)?; Some(commands) } fn extract_commands_from_program(program: &ast::Program, commands: &mut Vec<String>) -> Option<()> { for complete_command in &program.complete_commands { extract_commands_from_compound_list(complete_command, commands)?; } Some(()) } fn extract_commands_from_compound_list( compound_list: &ast::CompoundList, commands: &mut Vec<String>, ) -> Option<()> { for item in &compound_list.0 { extract_commands_from_and_or_list(&item.0, commands)?; } Some(()) } fn extract_commands_from_and_or_list( and_or_list: &ast::AndOrList, commands: &mut Vec<String>, ) -> Option<()> { extract_commands_from_pipeline(&and_or_list.first, commands)?; for and_or in &and_or_list.additional { match and_or { ast::AndOr::And(pipeline) | ast::AndOr::Or(pipeline) => { extract_commands_from_pipeline(pipeline, commands)?; } } } Some(()) } fn extract_commands_from_pipeline( pipeline: &ast::Pipeline, commands: &mut Vec<String>, ) -> Option<()> { for command in &pipeline.seq { extract_commands_from_command(command, commands)?; } Some(()) } fn extract_commands_from_command(command: &ast::Command, commands: &mut Vec<String>) -> Option<()> { match command { ast::Command::Simple(simple_command) => { extract_commands_from_simple_command(simple_command, commands)?; } ast::Command::Compound(compound_command, redirect_list) => { let body_start = extract_commands_from_compound_command(compound_command, commands)?; if let Some(redirect_list) = redirect_list { let mut normalized_redirects = Vec::new(); for redirect in &redirect_list.0 { match normalize_io_redirect(redirect)? { RedirectNormalization::Normalized(s) => normalized_redirects.push(s), RedirectNormalization::Skip => {} } } if !normalized_redirects.is_empty() { if body_start >= commands.len() { return None; } commands.extend(normalized_redirects); } for redirect in &redirect_list.0 { extract_commands_from_io_redirect(redirect, commands)?; } } } ast::Command::Function(func_def) => { extract_commands_from_function_body(&func_def.body, commands)?; } ast::Command::ExtendedTest(test_expr) => { extract_commands_from_extended_test_expr(test_expr, commands)?; } } Some(()) } enum RedirectNormalization { Normalized(String), Skip, } fn extract_commands_from_simple_command( simple_command: &ast::SimpleCommand, commands: &mut Vec<String>, ) -> Option<()> { // Build a normalized command string from individual words, stripping shell // quotes so that security patterns match regardless of quoting style. // For example, both `rm -rf '/'` and `rm -rf /` normalize to "rm -rf /". // // If any word fails to normalize, we return None so that `extract_commands` // returns None — the same as a shell parse failure. The caller then falls // back to raw-input matching with always_allow disabled. let mut words = Vec::new(); let mut redirects = Vec::new(); if let Some(prefix) = &simple_command.prefix { for item in &prefix.0 { if let ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) = item { match normalize_io_redirect(redirect) { Some(RedirectNormalization::Normalized(s)) => redirects.push(s), Some(RedirectNormalization::Skip) => {} None => return None, } } } } if let Some(word) = &simple_command.word_or_name { words.push(normalize_word(word)?); } if let Some(suffix) = &simple_command.suffix { for item in &suffix.0 { match item { ast::CommandPrefixOrSuffixItem::Word(word) => { words.push(normalize_word(word)?); } ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) => { match normalize_io_redirect(redirect) { Some(RedirectNormalization::Normalized(s)) => redirects.push(s), Some(RedirectNormalization::Skip) => {} None => return None, } } _ => {} } } } if words.is_empty() && !redirects.is_empty() { return None; } let command_str = words.join(" "); if !command_str.is_empty() { commands.push(command_str); } commands.extend(redirects); // Extract nested commands from command substitutions, process substitutions, etc. if let Some(prefix) = &simple_command.prefix { extract_commands_from_command_prefix(prefix, commands)?; } if let Some(word) = &simple_command.word_or_name { extract_commands_from_word(word, commands)?; } if let Some(suffix) = &simple_command.suffix { extract_commands_from_command_suffix(suffix, commands)?; } Some(()) } /// Normalizes a shell word by stripping quoting syntax and returning the /// semantic (unquoted) value. Returns `None` if word parsing fails. fn normalize_word(word: &ast::Word) -> Option<String> { let options = ParserOptions::default(); let pieces = brush_parser::word::parse(&word.value, &options).ok()?; let mut result = String::new(); for piece_with_source in &pieces { normalize_word_piece_into( &piece_with_source.piece, &word.value, piece_with_source.start_index, piece_with_source.end_index, &mut result, )?; } Some(result) } fn normalize_word_piece_into( piece: &WordPiece, raw_value: &str, start_index: usize, end_index: usize, result: &mut String, ) -> Option<()> { match piece { WordPiece::Text(text) => result.push_str(text), WordPiece::SingleQuotedText(text) => result.push_str(text), WordPiece::AnsiCQuotedText(text) => result.push_str(text), WordPiece::EscapeSequence(text) => { result.push_str(text.strip_prefix('\\').unwrap_or(text)); } WordPiece::DoubleQuotedSequence(pieces) | WordPiece::GettextDoubleQuotedSequence(pieces) => { for inner in pieces { normalize_word_piece_into( &inner.piece, raw_value, inner.start_index, inner.end_index, result, )?; } } WordPiece::TildePrefix(prefix) => { result.push('~'); result.push_str(prefix); } // For parameter expansions, command substitutions, and arithmetic expressions, // preserve the original source text so that patterns like `\$HOME` continue // to match. WordPiece::ParameterExpansion(_) | WordPiece::CommandSubstitution(_) | WordPiece::BackquotedCommandSubstitution(_) | WordPiece::ArithmeticExpression(_) => { let source = raw_value.get(start_index..end_index)?; result.push_str(source); } } Some(()) } fn normalize_io_redirect(redirect: &ast::IoRedirect) -> Option<RedirectNormalization> { match redirect { ast::IoRedirect::File(fd, kind, target) => { let target_word = match target { ast::IoFileRedirectTarget::Filename(word) => word, _ => return Some(RedirectNormalization::Skip), }; let operator = match kind { ast::IoFileRedirectKind::Read => "<", ast::IoFileRedirectKind::Write => ">", ast::IoFileRedirectKind::Append => ">>", ast::IoFileRedirectKind::ReadAndWrite => "<>", ast::IoFileRedirectKind::Clobber => ">|", // The parser pairs DuplicateInput/DuplicateOutput with // IoFileRedirectTarget::Duplicate (not Filename), so the // target match above will return Skip before we reach here. // These arms are kept for defensiveness. ast::IoFileRedirectKind::DuplicateInput => "<&", ast::IoFileRedirectKind::DuplicateOutput => ">&", }; let fd_prefix = match fd { Some(fd) => fd.to_string(), None => String::new(), }; let normalized = normalize_word(target_word)?; Some(RedirectNormalization::Normalized(format!( "{}{} {}", fd_prefix, operator, normalized ))) } ast::IoRedirect::OutputAndError(word, append) => { let operator = if *append { "&>>" } else { "&>" }; let normalized = normalize_word(word)?; Some(RedirectNormalization::Normalized(format!( "{} {}", operator, normalized ))) } ast::IoRedirect::HereDocument(_, _) | ast::IoRedirect::HereString(_, _) => { Some(RedirectNormalization::Skip) } } } fn extract_commands_from_command_prefix( prefix: &ast::CommandPrefix, commands: &mut Vec<String>, ) -> Option<()> { for item in &prefix.0 { extract_commands_from_prefix_or_suffix_item(item, commands)?; } Some(()) } fn extract_commands_from_command_suffix( suffix: &ast::CommandSuffix, commands: &mut Vec<String>, ) -> Option<()> { for item in &suffix.0 { extract_commands_from_prefix_or_suffix_item(item, commands)?; } Some(()) } fn extract_commands_from_prefix_or_suffix_item( item: &ast::CommandPrefixOrSuffixItem, commands: &mut Vec<String>, ) -> Option<()> { match item { ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) => { extract_commands_from_io_redirect(redirect, commands)?; } ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => { extract_commands_from_assignment(assignment, commands)?; } ast::CommandPrefixOrSuffixItem::Word(word) => { extract_commands_from_word(word, commands)?; } ast::CommandPrefixOrSuffixItem::ProcessSubstitution(_kind, subshell) => { extract_commands_from_compound_list(&subshell.list, commands)?; } } Some(()) } fn extract_commands_from_io_redirect( redirect: &ast::IoRedirect, commands: &mut Vec<String>, ) -> Option<()> { match redirect { ast::IoRedirect::File(_fd, _kind, target) => match target { ast::IoFileRedirectTarget::ProcessSubstitution(_kind, subshell) => { extract_commands_from_compound_list(&subshell.list, commands)?; } ast::IoFileRedirectTarget::Filename(word) => { extract_commands_from_word(word, commands)?; } _ => {} }, ast::IoRedirect::HereDocument(_fd, here_doc) => { if here_doc.requires_expansion { extract_commands_from_word(&here_doc.doc, commands)?; } } ast::IoRedirect::HereString(_fd, word) => { extract_commands_from_word(word, commands)?; } ast::IoRedirect::OutputAndError(word, _) => { extract_commands_from_word(word, commands)?; } } Some(()) } fn extract_commands_from_assignment( assignment: &ast::Assignment, commands: &mut Vec<String>, ) -> Option<()> { match &assignment.value { ast::AssignmentValue::Scalar(word) => { extract_commands_from_word(word, commands)?; } ast::AssignmentValue::Array(words) => { for (opt_word, word) in words { if let Some(w) = opt_word { extract_commands_from_word(w, commands)?; } extract_commands_from_word(word, commands)?; } } } Some(()) } fn extract_commands_from_word(word: &ast::Word, commands: &mut Vec<String>) -> Option<()> { let options = ParserOptions::default(); let pieces = brush_parser::word::parse(&word.value, &options).ok()?; for piece_with_source in pieces { extract_commands_from_word_piece(&piece_with_source.piece, commands)?; } Some(()) } fn extract_commands_from_word_piece(piece: &WordPiece, commands: &mut Vec<String>) -> Option<()> { match piece { WordPiece::CommandSubstitution(cmd_str) | WordPiece::BackquotedCommandSubstitution(cmd_str) => { let nested_commands = extract_commands(cmd_str)?; commands.extend(nested_commands); } WordPiece::DoubleQuotedSequence(pieces) | WordPiece::GettextDoubleQuotedSequence(pieces) => { for inner_piece_with_source in pieces { extract_commands_from_word_piece(&inner_piece_with_source.piece, commands)?; } } WordPiece::EscapeSequence(_) | WordPiece::SingleQuotedText(_) | WordPiece::Text(_) | WordPiece::AnsiCQuotedText(_) | WordPiece::TildePrefix(_) | WordPiece::ParameterExpansion(_) | WordPiece::ArithmeticExpression(_) => {} } Some(()) } fn extract_commands_from_compound_command( compound_command: &ast::CompoundCommand, commands: &mut Vec<String>, ) -> Option<usize> { match compound_command { ast::CompoundCommand::BraceGroup(brace_group) => { let body_start = commands.len(); extract_commands_from_compound_list(&brace_group.list, commands)?; Some(body_start) } ast::CompoundCommand::Subshell(subshell) => { let body_start = commands.len(); extract_commands_from_compound_list(&subshell.list, commands)?; Some(body_start) } ast::CompoundCommand::ForClause(for_clause) => { if let Some(words) = &for_clause.values { for word in words { extract_commands_from_word(word, commands)?; } } let body_start = commands.len(); extract_commands_from_do_group(&for_clause.body, commands)?; Some(body_start) } ast::CompoundCommand::CaseClause(case_clause) => { extract_commands_from_word(&case_clause.value, commands)?; let body_start = commands.len(); for item in &case_clause.cases { if let Some(body) = &item.cmd { extract_commands_from_compound_list(body, commands)?; } } Some(body_start) } ast::CompoundCommand::IfClause(if_clause) => { extract_commands_from_compound_list(&if_clause.condition, commands)?; let body_start = commands.len(); extract_commands_from_compound_list(&if_clause.then, commands)?; if let Some(elses) = &if_clause.elses { for else_item in elses { if let Some(condition) = &else_item.condition { extract_commands_from_compound_list(condition, commands)?; } extract_commands_from_compound_list(&else_item.body, commands)?; } } Some(body_start) } ast::CompoundCommand::WhileClause(while_clause) | ast::CompoundCommand::UntilClause(while_clause) => { extract_commands_from_compound_list(&while_clause.0, commands)?; let body_start = commands.len(); extract_commands_from_do_group(&while_clause.1, commands)?; Some(body_start) } ast::CompoundCommand::ArithmeticForClause(arith_for) => { let body_start = commands.len(); extract_commands_from_do_group(&arith_for.body, commands)?; Some(body_start) } ast::CompoundCommand::Arithmetic(_arith_cmd) => Some(commands.len()), } } fn extract_commands_from_do_group( do_group: &ast::DoGroupCommand, commands: &mut Vec<String>, ) -> Option<()> { extract_commands_from_compound_list(&do_group.list, commands) } fn extract_commands_from_function_body( func_body: &ast::FunctionBody, commands: &mut Vec<String>, ) -> Option<()> { let body_start = extract_commands_from_compound_command(&func_body.0, commands)?; if let Some(redirect_list) = &func_body.1 { let mut normalized_redirects = Vec::new(); for redirect in &redirect_list.0 { match normalize_io_redirect(redirect)? { RedirectNormalization::Normalized(s) => normalized_redirects.push(s), RedirectNormalization::Skip => {} } } if !normalized_redirects.is_empty() { if body_start >= commands.len() { return None; } commands.extend(normalized_redirects); } for redirect in &redirect_list.0 { extract_commands_from_io_redirect(redirect, commands)?; } } Some(()) } fn extract_commands_from_extended_test_expr(