@@ -158,27 +158,30 @@ impl Builtin for Read {
158158 var_args
159159 } ;
160160
161- // Assign words to variables
161+ // Assign words to variables via side effects (respects local scoping)
162+ let mut result = ExecResult :: ok ( String :: new ( ) ) ;
162163 for ( i, var_name) in var_names. iter ( ) . enumerate ( ) {
163164 // THREAT[TM-INJ-009]: Block internal variable prefix injection via read
164165 if is_internal_variable ( var_name) {
165166 continue ;
166167 }
167- if i == var_names. len ( ) - 1 {
168+ let value = if i == var_names. len ( ) - 1 {
168169 // Last variable gets all remaining words
169170 let remaining: Vec < & str > = words. iter ( ) . skip ( i) . copied ( ) . collect ( ) ;
170- let value = remaining. join ( " " ) ;
171- ctx. variables . insert ( var_name. to_string ( ) , value) ;
171+ remaining. join ( " " )
172172 } else if i < words. len ( ) {
173- ctx. variables
174- . insert ( var_name. to_string ( ) , words[ i] . to_string ( ) ) ;
173+ words[ i] . to_string ( )
175174 } else {
176175 // Not enough words - set to empty
177- ctx. variables . insert ( var_name. to_string ( ) , String :: new ( ) ) ;
178- }
176+ String :: new ( )
177+ } ;
178+ result. side_effects . push ( BuiltinSideEffect :: SetVariable {
179+ name : var_name. to_string ( ) ,
180+ value,
181+ } ) ;
179182 }
180183
181- Ok ( ExecResult :: ok ( String :: new ( ) ) )
184+ Ok ( result )
182185 }
183186}
184187
@@ -199,6 +202,17 @@ mod tests {
199202 ( fs, cwd, variables)
200203 }
201204
205+ /// Extract SetVariable side effects into a map for easy assertion.
206+ fn extract_vars ( result : & ExecResult ) -> HashMap < String , String > {
207+ let mut map = HashMap :: new ( ) ;
208+ for effect in & result. side_effects {
209+ if let BuiltinSideEffect :: SetVariable { name, value } = effect {
210+ map. insert ( name. clone ( ) , value. clone ( ) ) ;
211+ }
212+ }
213+ map
214+ }
215+
202216 // ==================== no stdin ====================
203217
204218 #[ tokio:: test]
@@ -228,7 +242,8 @@ mod tests {
228242 ) ;
229243 let result = Read . execute ( ctx) . await . unwrap ( ) ;
230244 assert_eq ! ( result. exit_code, 0 ) ;
231- assert_eq ! ( variables. get( "REPLY" ) . unwrap( ) , "hello world" ) ;
245+ let vars = extract_vars ( & result) ;
246+ assert_eq ! ( vars. get( "REPLY" ) . unwrap( ) , "hello world" ) ;
232247 }
233248
234249 // ==================== read into named variable ====================
@@ -248,7 +263,8 @@ mod tests {
248263 ) ;
249264 let result = Read . execute ( ctx) . await . unwrap ( ) ;
250265 assert_eq ! ( result. exit_code, 0 ) ;
251- assert_eq ! ( variables. get( "MY_VAR" ) . unwrap( ) , "test_value" ) ;
266+ let vars = extract_vars ( & result) ;
267+ assert_eq ! ( vars. get( "MY_VAR" ) . unwrap( ) , "test_value" ) ;
252268 }
253269
254270 // ==================== read into multiple variables ====================
@@ -268,10 +284,11 @@ mod tests {
268284 ) ;
269285 let result = Read . execute ( ctx) . await . unwrap ( ) ;
270286 assert_eq ! ( result. exit_code, 0 ) ;
271- assert_eq ! ( variables. get( "A" ) . unwrap( ) , "one" ) ;
272- assert_eq ! ( variables. get( "B" ) . unwrap( ) , "two" ) ;
287+ let vars = extract_vars ( & result) ;
288+ assert_eq ! ( vars. get( "A" ) . unwrap( ) , "one" ) ;
289+ assert_eq ! ( vars. get( "B" ) . unwrap( ) , "two" ) ;
273290 // Last var gets remaining words
274- assert_eq ! ( variables . get( "C" ) . unwrap( ) , "three four" ) ;
291+ assert_eq ! ( vars . get( "C" ) . unwrap( ) , "three four" ) ;
275292 }
276293
277294 #[ tokio:: test]
@@ -289,9 +306,10 @@ mod tests {
289306 ) ;
290307 let result = Read . execute ( ctx) . await . unwrap ( ) ;
291308 assert_eq ! ( result. exit_code, 0 ) ;
292- assert_eq ! ( variables. get( "A" ) . unwrap( ) , "one" ) ;
293- assert_eq ! ( variables. get( "B" ) . unwrap( ) , "" ) ;
294- assert_eq ! ( variables. get( "C" ) . unwrap( ) , "" ) ;
309+ let vars = extract_vars ( & result) ;
310+ assert_eq ! ( vars. get( "A" ) . unwrap( ) , "one" ) ;
311+ assert_eq ! ( vars. get( "B" ) . unwrap( ) , "" ) ;
312+ assert_eq ! ( vars. get( "C" ) . unwrap( ) , "" ) ;
295313 }
296314
297315 // ==================== -r flag (raw mode) ====================
@@ -311,7 +329,8 @@ mod tests {
311329 ) ;
312330 let result = Read . execute ( ctx) . await . unwrap ( ) ;
313331 assert_eq ! ( result. exit_code, 0 ) ;
314- assert_eq ! ( variables. get( "LINE" ) . unwrap( ) , "hello\\ world" ) ;
332+ let vars = extract_vars ( & result) ;
333+ assert_eq ! ( vars. get( "LINE" ) . unwrap( ) , "hello\\ world" ) ;
315334 }
316335
317336 #[ tokio:: test]
@@ -329,8 +348,9 @@ mod tests {
329348 ) ;
330349 let result = Read . execute ( ctx) . await . unwrap ( ) ;
331350 assert_eq ! ( result. exit_code, 0 ) ;
351+ let vars = extract_vars ( & result) ;
332352 // Without -r, backslash-newline is line continuation
333- assert_eq ! ( variables . get( "LINE" ) . unwrap( ) , "helloworld" ) ;
353+ assert_eq ! ( vars . get( "LINE" ) . unwrap( ) , "helloworld" ) ;
334354 }
335355
336356 // ==================== -n flag (read N chars) ====================
@@ -350,7 +370,8 @@ mod tests {
350370 ) ;
351371 let result = Read . execute ( ctx) . await . unwrap ( ) ;
352372 assert_eq ! ( result. exit_code, 0 ) ;
353- assert_eq ! ( variables. get( "CHUNK" ) . unwrap( ) , "abc" ) ;
373+ let vars = extract_vars ( & result) ;
374+ assert_eq ! ( vars. get( "CHUNK" ) . unwrap( ) , "abc" ) ;
354375 }
355376
356377 #[ tokio:: test]
@@ -368,7 +389,8 @@ mod tests {
368389 ) ;
369390 let result = Read . execute ( ctx) . await . unwrap ( ) ;
370391 assert_eq ! ( result. exit_code, 0 ) ;
371- assert_eq ! ( variables. get( "CHUNK" ) . unwrap( ) , "hi" ) ;
392+ let vars = extract_vars ( & result) ;
393+ assert_eq ! ( vars. get( "CHUNK" ) . unwrap( ) , "hi" ) ;
372394 }
373395
374396 // ==================== -d flag (delimiter) ====================
@@ -388,7 +410,8 @@ mod tests {
388410 ) ;
389411 let result = Read . execute ( ctx) . await . unwrap ( ) ;
390412 assert_eq ! ( result. exit_code, 0 ) ;
391- assert_eq ! ( variables. get( "FIELD" ) . unwrap( ) , "first" ) ;
413+ let vars = extract_vars ( & result) ;
414+ assert_eq ! ( vars. get( "FIELD" ) . unwrap( ) , "first" ) ;
392415 }
393416
394417 // ==================== -a flag (array mode) ====================
@@ -458,7 +481,8 @@ mod tests {
458481 ) ;
459482 let result = Read . execute ( ctx) . await . unwrap ( ) ;
460483 assert_eq ! ( result. exit_code, 0 ) ;
461- assert_eq ! ( variables. get( "V" ) . unwrap( ) , "path\\ to\\ file" ) ;
484+ let vars = extract_vars ( & result) ;
485+ assert_eq ! ( vars. get( "V" ) . unwrap( ) , "path\\ to\\ file" ) ;
462486 }
463487
464488 // ==================== multiline input ====================
@@ -478,7 +502,8 @@ mod tests {
478502 ) ;
479503 let result = Read . execute ( ctx) . await . unwrap ( ) ;
480504 assert_eq ! ( result. exit_code, 0 ) ;
481- assert_eq ! ( variables. get( "LINE" ) . unwrap( ) , "first" ) ;
505+ let vars = extract_vars ( & result) ;
506+ assert_eq ! ( vars. get( "LINE" ) . unwrap( ) , "first" ) ;
482507 }
483508
484509 // ==================== custom IFS ====================
@@ -499,8 +524,9 @@ mod tests {
499524 ) ;
500525 let result = Read . execute ( ctx) . await . unwrap ( ) ;
501526 assert_eq ! ( result. exit_code, 0 ) ;
502- assert_eq ! ( variables. get( "A" ) . unwrap( ) , "foo" ) ;
503- assert_eq ! ( variables. get( "B" ) . unwrap( ) , "bar baz" ) ;
527+ let vars = extract_vars ( & result) ;
528+ assert_eq ! ( vars. get( "A" ) . unwrap( ) , "foo" ) ;
529+ assert_eq ! ( vars. get( "B" ) . unwrap( ) , "bar baz" ) ;
504530 }
505531
506532 #[ tokio:: test]
@@ -524,10 +550,11 @@ mod tests {
524550 ) ;
525551 let result = Read . execute ( ctx) . await . unwrap ( ) ;
526552 assert_eq ! ( result. exit_code, 0 ) ;
527- assert_eq ! ( variables. get( "A" ) . unwrap( ) , "one" ) ;
528- assert_eq ! ( variables. get( "B" ) . unwrap( ) , "" ) ;
529- assert_eq ! ( variables. get( "C" ) . unwrap( ) , "three" ) ;
530- assert_eq ! ( variables. get( "D" ) . unwrap( ) , "" ) ;
553+ let vars = extract_vars ( & result) ;
554+ assert_eq ! ( vars. get( "A" ) . unwrap( ) , "one" ) ;
555+ assert_eq ! ( vars. get( "B" ) . unwrap( ) , "" ) ;
556+ assert_eq ! ( vars. get( "C" ) . unwrap( ) , "three" ) ;
557+ assert_eq ! ( vars. get( "D" ) . unwrap( ) , "" ) ;
531558 }
532559
533560 #[ tokio:: test]
@@ -546,6 +573,7 @@ mod tests {
546573 ) ;
547574 let result = Read . execute ( ctx) . await . unwrap ( ) ;
548575 assert_eq ! ( result. exit_code, 0 ) ;
549- assert_eq ! ( variables. get( "LINE" ) . unwrap( ) , "no splitting here" ) ;
576+ let vars = extract_vars ( & result) ;
577+ assert_eq ! ( vars. get( "LINE" ) . unwrap( ) , "no splitting here" ) ;
550578 }
551579}
0 commit comments