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