@@ -181,6 +181,21 @@ pub fn run_with_options(endpoint: &dyn Endpoint, config: &Config, options: &RunO
181181 }
182182 }
183183
184+ // When extract_commands simplifies a compound shell construct (e.g. a for-loop)
185+ // down to a single sub-command, evaluate that sub-command instead of the
186+ // original input string.
187+ let effective_command = if commands. len ( ) == 1 && commands[ 0 ] != command {
188+ if options. verbose {
189+ eprintln ! (
190+ "[verbose] Single sub-command extracted: {:?} (from {:?})" ,
191+ commands[ 0 ] , command
192+ ) ;
193+ }
194+ & commands[ 0 ]
195+ } else {
196+ & command
197+ } ;
198+
184199 let action_result = if commands. len ( ) > 1 {
185200 match evaluate_compound ( config, & command, & context) {
186201 Ok ( compound_result) => {
@@ -198,7 +213,7 @@ pub fn run_with_options(endpoint: &dyn Endpoint, config: &Config, options: &RunO
198213 Err ( e) => return endpoint. handle_error ( e. into ( ) ) ,
199214 }
200215 } else {
201- match evaluate_command ( config, & command , & context) {
216+ match evaluate_command ( config, effective_command , & context) {
202217 Ok ( result) => {
203218 if options. verbose {
204219 log_matched_rules ( & result. matched_rules ) ;
@@ -885,6 +900,67 @@ mod tests {
885900 }
886901 }
887902
903+ // --- single extracted sub-command uses simplified form ---
904+
905+ #[ rstest]
906+ #[ case:: for_loop_with_echo(
907+ "for f in *.yaml; do echo $f; done" ,
908+ allow_rule( "echo *" ) ,
909+ true , // expect handle_action
910+ false , // expect handle_no_match
911+ ) ]
912+ #[ case:: for_loop_no_matching_rule(
913+ "for f in *.yaml; do echo $f; done" ,
914+ allow_rule( "git status" ) ,
915+ false ,
916+ true
917+ ) ]
918+ fn single_extracted_subcommand_evaluates_simplified_form (
919+ #[ case] command : & str ,
920+ #[ case] rule : RuleEntry ,
921+ #[ case] expect_action : bool ,
922+ #[ case] expect_no_match : bool ,
923+ ) {
924+ let endpoint = MockEndpoint :: new ( Ok ( Some ( command. to_string ( ) ) ) ) ;
925+ let config = make_config ( vec ! [ rule] ) ;
926+ run ( & endpoint, & config) ;
927+
928+ assert_eq ! ( * endpoint. called_handle_action. borrow( ) , expect_action) ;
929+ assert_eq ! ( * endpoint. called_handle_no_match. borrow( ) , expect_no_match) ;
930+ }
931+
932+ #[ rstest]
933+ fn for_loop_with_deny_rule_on_subcommand ( ) {
934+ let endpoint =
935+ MockEndpoint :: new ( Ok ( Some ( "for f in *.yaml; do rm -rf $f; done" . to_string ( ) ) ) ) ;
936+ let config = make_config ( vec ! [ deny_rule( "rm *" ) ] ) ;
937+ run ( & endpoint, & config) ;
938+
939+ assert ! ( * endpoint. called_handle_action. borrow( ) ) ;
940+ assert ! ( matches!(
941+ * endpoint. last_action. borrow( ) ,
942+ Some ( Action :: Deny ( _) )
943+ ) ) ;
944+ }
945+
946+ #[ rstest]
947+ fn single_extracted_subcommand_verbose_logs_extraction ( ) {
948+ let endpoint = MockEndpoint :: new ( Ok ( Some ( "for f in *.yaml; do echo $f; done" . to_string ( ) ) ) ) ;
949+ let config = make_config ( vec ! [ allow_rule( "echo *" ) ] ) ;
950+ let options = RunOptions {
951+ dry_run : false ,
952+ verbose : true ,
953+ } ;
954+ let exit_code = run_with_options ( & endpoint, & config, & options) ;
955+
956+ assert ! ( * endpoint. called_handle_action. borrow( ) ) ;
957+ assert ! ( matches!(
958+ * endpoint. last_action. borrow( ) ,
959+ Some ( Action :: Allow )
960+ ) ) ;
961+ assert_eq ! ( exit_code, 0 ) ;
962+ }
963+
888964 // --- apply_sandbox_fallback unit tests ---
889965
890966 #[ rstest]
0 commit comments