@@ -2786,16 +2786,33 @@ impl Interpreter {
27862786 } ) ;
27872787
27882788 // Save and apply shell options (-e, -x, -u, -o pipefail, etc.)
2789+ // Also save/restore OPTIND so getopts state doesn't leak between scripts
27892790 let mut saved_opts: Vec < ( String , Option < String > ) > = Vec :: new ( ) ;
27902791 for ( var, val) in & shell_opts {
27912792 let prev = self . variables . get ( * var) . cloned ( ) ;
27922793 saved_opts. push ( ( var. to_string ( ) , prev) ) ;
27932794 self . variables . insert ( var. to_string ( ) , val. to_string ( ) ) ;
27942795 }
2796+ let saved_optind = self . variables . get ( "OPTIND" ) . cloned ( ) ;
2797+ let saved_optchar = self . variables . get ( "_OPTCHAR_IDX" ) . cloned ( ) ;
2798+ self . variables . insert ( "OPTIND" . to_string ( ) , "1" . to_string ( ) ) ;
2799+ self . variables . remove ( "_OPTCHAR_IDX" ) ;
27952800
27962801 // Execute the script
27972802 let result = self . execute ( & script) . await ;
27982803
2804+ // Restore OPTIND and internal getopts state
2805+ if let Some ( val) = saved_optind {
2806+ self . variables . insert ( "OPTIND" . to_string ( ) , val) ;
2807+ } else {
2808+ self . variables . remove ( "OPTIND" ) ;
2809+ }
2810+ if let Some ( val) = saved_optchar {
2811+ self . variables . insert ( "_OPTCHAR_IDX" . to_string ( ) , val) ;
2812+ } else {
2813+ self . variables . remove ( "_OPTCHAR_IDX" ) ;
2814+ }
2815+
27992816 // Restore shell options
28002817 for ( var, prev) in saved_opts {
28012818 if let Some ( val) = prev {
@@ -9561,4 +9578,106 @@ mod tests {
95619578 // With the fix, the cycle is detected and 'a' resolves to itself.
95629579 assert_eq ! ( result. exit_code, 0 ) ;
95639580 }
9581+
9582+ #[ tokio:: test]
9583+ async fn test_getopts_while_loop ( ) {
9584+ // Issue #397: getopts in while loop should iterate over all options
9585+ let mut bash = crate :: Bash :: new ( ) ;
9586+ let result = bash
9587+ . exec (
9588+ r#"
9589+ set -- -f json -v
9590+ while getopts "f:vh" opt; do
9591+ case "$opt" in
9592+ f) FORMAT="$OPTARG" ;;
9593+ v) VERBOSE=1 ;;
9594+ esac
9595+ done
9596+ echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9597+ "# ,
9598+ )
9599+ . await
9600+ . unwrap ( ) ;
9601+ assert_eq ! ( result. exit_code, 0 ) ;
9602+ assert_eq ! ( result. stdout. trim( ) , "FORMAT=json VERBOSE=1" ) ;
9603+ }
9604+
9605+ #[ tokio:: test]
9606+ async fn test_getopts_script_with_args ( ) {
9607+ // Issue #397: getopts via bash -c with script args
9608+ let mut bash = crate :: Bash :: new ( ) ;
9609+ // Write a script that uses getopts, then invoke it with arguments
9610+ let result = bash
9611+ . exec (
9612+ r#"
9613+ cat > /tmp/test_getopts.sh << 'SCRIPT'
9614+ while getopts "f:vh" opt; do
9615+ case "$opt" in
9616+ f) FORMAT="$OPTARG" ;;
9617+ v) VERBOSE=1 ;;
9618+ esac
9619+ done
9620+ echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9621+ SCRIPT
9622+ bash /tmp/test_getopts.sh -f json -v
9623+ "# ,
9624+ )
9625+ . await
9626+ . unwrap ( ) ;
9627+ assert_eq ! ( result. stdout. trim( ) , "FORMAT=json VERBOSE=1" ) ;
9628+ }
9629+
9630+ #[ tokio:: test]
9631+ async fn test_getopts_bash_c_with_args ( ) {
9632+ // Issue #397: getopts via bash -c 'script' -- args
9633+ let mut bash = crate :: Bash :: new ( ) ;
9634+ let result = bash
9635+ . exec (
9636+ r#"bash -c '
9637+ FORMAT="csv"
9638+ VERBOSE=0
9639+ while getopts "f:vh" opt; do
9640+ case "$opt" in
9641+ f) FORMAT="$OPTARG" ;;
9642+ v) VERBOSE=1 ;;
9643+ esac
9644+ done
9645+ echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9646+ ' -- -f json -v"# ,
9647+ )
9648+ . await
9649+ . unwrap ( ) ;
9650+ assert_eq ! ( result. stdout. trim( ) , "FORMAT=json VERBOSE=1" ) ;
9651+ }
9652+
9653+ #[ tokio:: test]
9654+ async fn test_getopts_optind_reset_between_scripts ( ) {
9655+ // Issue #397: OPTIND persists across bash script invocations, causing
9656+ // getopts to skip all options on the second run
9657+ let mut bash = crate :: Bash :: new ( ) ;
9658+ let result = bash
9659+ . exec (
9660+ r#"
9661+ cat > /tmp/opts.sh << 'SCRIPT'
9662+ FORMAT="csv"
9663+ VERBOSE=0
9664+ while getopts "f:vh" opt; do
9665+ case "$opt" in
9666+ f) FORMAT="$OPTARG" ;;
9667+ v) VERBOSE=1 ;;
9668+ esac
9669+ done
9670+ echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9671+ SCRIPT
9672+ bash /tmp/opts.sh -f json -v
9673+ bash /tmp/opts.sh -f xml -v
9674+ "# ,
9675+ )
9676+ . await
9677+ . unwrap ( ) ;
9678+ let lines: Vec < & str > = result. stdout . trim ( ) . lines ( ) . collect ( ) ;
9679+ assert_eq ! ( lines. len( ) , 2 , "expected 2 lines: {}" , result. stdout) ;
9680+ assert_eq ! ( lines[ 0 ] , "FORMAT=json VERBOSE=1" ) ;
9681+ assert_eq ! ( lines[ 1 ] , "FORMAT=xml VERBOSE=1" ) ;
9682+ }
95649683}
0 commit comments