Fortran · 54397 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: control_flow
3 ! Purpose: Shell scripting control flow structures (if/then/else, loops)
4 ! ==============================================================================
5 module control_flow
6 use shell_types
7 use system_interface
8 use advanced_test, only: evaluate_test_expression
9 use variables, only: set_shell_variable, get_shell_variable
10 use glob, only: pattern_matches_no_dotfile_check
11 use iso_fortran_env, only: output_unit, error_unit
12 implicit none
13
14 ! Interface for the evaluate_condition procedure
15 abstract interface
16 subroutine evaluate_condition_interface(condition_cmd, shell, result)
17 import :: shell_state_t
18 character(len=*), intent(in) :: condition_cmd
19 type(shell_state_t), intent(inout) :: shell
20 logical, intent(out) :: result
21 end subroutine
22 end interface
23
24 ! Procedure pointer for evaluate_condition (set by executor to avoid circular dependency)
25 procedure(evaluate_condition_interface), pointer, public :: evaluate_condition => null()
26
27 ! Make simple_variable_expand accessible to executor
28 public :: simple_variable_expand
29
30 ! Control flow keywords
31 integer, parameter :: FLOW_IF = 1
32 integer, parameter :: FLOW_THEN = 2
33 integer, parameter :: FLOW_ELSE = 3
34 integer, parameter :: FLOW_ELIF = 15
35 integer, parameter :: FLOW_FI = 4
36 integer, parameter :: FLOW_WHILE = 5
37 integer, parameter :: FLOW_UNTIL = 16
38 integer, parameter :: FLOW_FOR = 6
39 integer, parameter :: FLOW_DO = 7
40 integer, parameter :: FLOW_DONE = 8
41 integer, parameter :: FLOW_FUNCTION = 9
42 integer, parameter :: FLOW_RETURN = 10
43 integer, parameter :: FLOW_LOCAL = 11
44 integer, parameter :: BLOCK_CASE = 12
45 integer, parameter :: FLOW_ESAC = 13
46 integer, parameter :: FLOW_IN = 14
47
48
49 type :: case_pattern_t
50 character(len=256) :: pattern
51 character(len=2048) :: commands
52 logical :: matched
53 end type case_pattern_t
54
55 type :: case_block_t
56 character(len=256) :: case_variable
57 type(case_pattern_t) :: patterns(50)
58 integer :: num_patterns
59 integer :: current_pattern
60 logical :: found_match
61 end type case_block_t
62
63 contains
64
65 function is_control_flow_keyword(word) result(is_flow)
66 character(len=*), intent(in) :: word
67 logical :: is_flow
68
69 is_flow = (trim(word) == 'if' .or. &
70 trim(word) == 'then' .or. &
71 trim(word) == 'else' .or. &
72 trim(word) == 'elif' .or. &
73 trim(word) == 'fi' .or. &
74 trim(word) == 'while' .or. &
75 trim(word) == 'until' .or. &
76 trim(word) == 'for' .or. &
77 trim(word) == 'do' .or. &
78 trim(word) == 'done' .or. &
79 trim(word) == 'function' .or. &
80 trim(word) == 'return' .or. &
81 trim(word) == 'local' .or. &
82 trim(word) == 'case' .or. &
83 trim(word) == 'esac' .or. &
84 trim(word) == 'in')
85 end function
86
87 function identify_flow_keyword(word) result(flow_type)
88 character(len=*), intent(in) :: word
89 integer :: flow_type
90
91 select case(trim(word))
92 case('if')
93 flow_type = FLOW_IF
94 case('then')
95 flow_type = FLOW_THEN
96 case('else')
97 flow_type = FLOW_ELSE
98 case('elif')
99 flow_type = FLOW_ELIF
100 case('fi')
101 flow_type = FLOW_FI
102 case('while')
103 flow_type = FLOW_WHILE
104 case('until')
105 flow_type = FLOW_UNTIL
106 case('for')
107 flow_type = FLOW_FOR
108 case('do')
109 flow_type = FLOW_DO
110 case('done')
111 flow_type = FLOW_DONE
112 case('function')
113 flow_type = FLOW_FUNCTION
114 case('return')
115 flow_type = FLOW_RETURN
116 case('local')
117 flow_type = FLOW_LOCAL
118 case('case')
119 flow_type = BLOCK_CASE
120 case('esac')
121 flow_type = FLOW_ESAC
122 case('in')
123 flow_type = FLOW_IN
124 case default
125 flow_type = 0
126 end select
127 end function
128
129 subroutine handle_if_statement(condition_cmd, shell, result)
130 character(len=*), intent(in) :: condition_cmd
131 type(shell_state_t), intent(inout) :: shell
132 logical, intent(out) :: result
133
134 ! For now, implement simple test conditions
135 ! Later this would parse and execute test expressions
136 if (index(condition_cmd, '-f') > 0) then
137 ! File exists test
138 result = .false. ! Simplified for now
139 else if (index(condition_cmd, '-d') > 0) then
140 ! Directory exists test
141 result = .false. ! Simplified for now
142 else
143 ! Default: treat as command and check exit status
144 ! This would execute the condition command and check its exit status
145 result = (shell%last_exit_status == 0)
146 end if
147 end subroutine
148
149 subroutine process_control_flow(cmd, shell, should_execute)
150 type(command_t), intent(in) :: cmd
151 type(shell_state_t), intent(inout) :: shell
152 logical, intent(out) :: should_execute
153
154 should_execute = .true. ! Default: execute normally
155
156 if (.not. allocated(cmd%tokens) .or. cmd%num_tokens == 0) return
157
158 ! Check for arithmetic for loop without space: for(( ...
159 if (len_trim(cmd%tokens(1)) >= 5) then
160 if (cmd%tokens(1)(1:5) == 'for((') then
161 call process_for_arith_statement(cmd, shell)
162 should_execute = .false. ! Don't execute the for command itself
163 return
164 end if
165 end if
166
167 ! Check for arithmetic for loop with space: for (( ...
168 if (cmd%num_tokens >= 2 .and. trim(cmd%tokens(1)) == 'for') then
169 if (len_trim(cmd%tokens(2)) >= 2 .and. cmd%tokens(2)(1:2) == '((') then
170 call process_for_arith_statement(cmd, shell)
171 should_execute = .false. ! Don't execute the for command itself
172 return
173 end if
174 end if
175
176 select case(trim(cmd%tokens(1)))
177 case('if')
178 call process_if_statement(cmd, shell, should_execute)
179 case('then')
180 call process_then_statement(shell, should_execute)
181 case('else')
182 call process_else_statement(shell, should_execute)
183 case('elif')
184 call process_elif_statement(cmd, shell, should_execute)
185 case('fi')
186 call process_fi_statement(shell, should_execute)
187 case('while')
188 call process_while_statement(cmd, shell, should_execute)
189 case('until')
190 call process_until_statement(cmd, shell, should_execute)
191 case('do')
192 call process_do_statement(cmd, shell, should_execute)
193 case('done')
194 call process_done_statement(shell, should_execute)
195 case('for')
196 call process_for_statement(cmd, shell, should_execute)
197 case('function')
198 call process_function_statement(cmd, shell, should_execute)
199 case('case')
200 call handle_case_statement(cmd, shell)
201 should_execute = .false. ! Don't execute 'case' as a command
202 case('esac')
203 call handle_esac_statement(shell)
204 should_execute = .false. ! Don't execute 'esac' as a command
205 case default
206 ! Check if line contains a case pattern (ends with ')' and we're in a case block)
207 ! NOTE: Fortran does not guarantee short-circuit evaluation of .and., so we must
208 ! use nested ifs to avoid accessing control_stack(0) when control_depth == 0
209 if (shell%control_depth > 0) then
210 if (shell%control_stack(shell%control_depth)%block_type == BLOCK_CASE) then
211 ! Check if this looks like a case pattern or ;;
212 if (trim(cmd%tokens(1)) == ';;') then
213 ! End of pattern commands - stop executing this case branch
214 shell%control_stack(shell%control_depth)%case_in_match = .false.
215 shell%control_stack(shell%control_depth)%condition_met = .false.
216 should_execute = .false. ! Don't execute ';;'
217 else if (index(cmd%tokens(1), ')') > 0 .and. cmd%num_tokens > 1) then
218 ! This is a pattern line (pattern is in first token)
219 ! with commands following it (e.g., "2) echo two")
220 call handle_case_pattern(cmd, shell)
221 ! Execute remaining tokens as command if pattern matched
222 should_execute = shell%control_stack(shell%control_depth)%case_in_match
223 shell%case_pattern_skip_first_token = should_execute ! Skip pattern token if executing
224 else if (index(cmd%tokens(1), ')') > 0) then
225 ! Pattern line with no following commands
226 call handle_case_pattern(cmd, shell)
227 should_execute = .false.
228 else
229 ! Regular command inside case - only execute if we're in a matched pattern
230 should_execute = shell%control_stack(shell%control_depth)%case_in_match
231 end if
232 else
233 ! Check if we should execute this command based on control flow state
234 should_execute = should_execute_command(shell)
235 end if
236 else
237 ! control_depth == 0, check normal control flow
238 should_execute = should_execute_command(shell)
239 end if
240 end select
241 end subroutine
242
243 subroutine process_if_statement(cmd, shell, should_execute)
244 type(command_t), intent(in) :: cmd
245 type(shell_state_t), intent(inout) :: shell
246 logical, intent(out) :: should_execute
247 logical :: condition_result
248 integer :: i
249 character(len=:), allocatable :: condition_cmd
250
251 should_execute = .false. ! Don't execute the if command itself
252
253 ! Parse if condition: if [ condition ] or if command
254 if (cmd%num_tokens < 2) then
255 write(error_unit, '(a)') 'if: missing condition'
256 shell%last_exit_status = 1
257 return
258 end if
259
260 ! Build condition command from tokens (skip "if")
261 condition_cmd = ''
262 do i = 2, cmd%num_tokens
263 if (len_trim(condition_cmd) > 0) then
264 condition_cmd = trim(condition_cmd) // ' ' // trim(cmd%tokens(i))
265 else
266 condition_cmd = trim(cmd%tokens(i))
267 end if
268 end do
269
270 ! Evaluate condition
271 call evaluate_condition(condition_cmd, shell, condition_result)
272
273 ! Push if block onto control stack
274 call push_control_block(shell, BLOCK_IF, condition_result)
275
276 ! POSIX: Reset exit status after evaluating control flow condition
277 ! The 'if' keyword itself doesn't fail - it just sets up control state
278 shell%last_exit_status = 0
279 end subroutine
280
281 subroutine process_while_statement(cmd, shell, should_execute)
282 type(command_t), intent(in) :: cmd
283 type(shell_state_t), intent(inout) :: shell
284 logical, intent(out) :: should_execute
285 logical :: condition_result
286 integer :: i
287 character(len=:), allocatable :: condition_cmd
288
289 should_execute = .false. ! Don't execute the while command itself
290
291 ! Parse while condition: while [ condition ] or while command
292 if (cmd%num_tokens < 2) then
293 write(error_unit, '(a)') 'while: missing condition'
294 shell%last_exit_status = 1
295 return
296 end if
297
298 ! Build condition command from tokens (skip "while")
299 condition_cmd = ''
300 do i = 2, cmd%num_tokens
301 if (len_trim(condition_cmd) > 0) then
302 condition_cmd = trim(condition_cmd) // ' ' // trim(cmd%tokens(i))
303 else
304 condition_cmd = trim(cmd%tokens(i))
305 end if
306 end do
307
308 ! Evaluate condition
309 call evaluate_condition(condition_cmd, shell, condition_result)
310
311 ! Push while block onto control stack
312 call push_control_block(shell, BLOCK_WHILE, condition_result)
313
314 ! IMPORTANT: Store the condition command for re-evaluation at each iteration
315 shell%control_stack(shell%control_depth)%condition_cmd = condition_cmd
316
317 ! POSIX: Reset exit status after evaluating control flow condition
318 ! The 'while' keyword itself doesn't fail - it just sets up control state
319 ! Re-evaluation at 'done' will execute the condition command fresh
320 shell%last_exit_status = 0
321 end subroutine
322
323 subroutine process_until_statement(cmd, shell, should_execute)
324 type(command_t), intent(in) :: cmd
325 type(shell_state_t), intent(inout) :: shell
326 logical, intent(out) :: should_execute
327 logical :: condition_result
328 integer :: i
329 character(len=:), allocatable :: condition_cmd
330
331 should_execute = .false. ! Don't execute the until command itself
332
333 ! Parse until condition: until [ condition ] or until command
334 if (cmd%num_tokens < 2) then
335 write(error_unit, '(a)') 'until: missing condition'
336 shell%last_exit_status = 1
337 return
338 end if
339
340 ! Build condition command from tokens (skip "until")
341 condition_cmd = ''
342 do i = 2, cmd%num_tokens
343 if (len_trim(condition_cmd) > 0) then
344 condition_cmd = trim(condition_cmd) // ' ' // trim(cmd%tokens(i))
345 else
346 condition_cmd = trim(cmd%tokens(i))
347 end if
348 end do
349
350 ! Evaluate condition
351 call evaluate_condition(condition_cmd, shell, condition_result)
352
353 ! Push until block onto control stack with INVERTED condition
354 ! until loops run while condition is FALSE
355 call push_control_block(shell, BLOCK_UNTIL, .not. condition_result)
356
357 ! IMPORTANT: Store the condition command for re-evaluation at each iteration
358 shell%control_stack(shell%control_depth)%condition_cmd = condition_cmd
359
360 ! POSIX: Reset exit status after evaluating control flow condition
361 ! The 'until' keyword itself doesn't fail - it just sets up control state
362 ! Re-evaluation at 'done' will execute the condition command fresh
363 shell%last_exit_status = 0
364 end subroutine
365
366 subroutine process_for_statement(cmd, shell, should_execute)
367 use substitution, only: expand_braces
368 use glob, only: glob_match, has_unescaped_glob_chars
369 type(command_t), intent(in) :: cmd
370 type(shell_state_t), intent(inout) :: shell
371 logical, intent(out) :: should_execute
372
373 character(len=MAX_TOKEN_LEN) :: var_name, list_part
374 character(len=MAX_TOKEN_LEN), allocatable :: expanded_items(:)
375 character(len=MAX_TOKEN_LEN), allocatable :: glob_matches(:)
376 character(len=MAX_TOKEN_LEN), allocatable :: final_items(:)
377 integer :: expanded_count, final_count, glob_count
378 integer :: i, j
379
380 should_execute = .false. ! Don't execute the for command itself
381
382 allocate(expanded_items(100))
383 allocate(glob_matches(100))
384 allocate(final_items(100))
385
386 ! Check if this is arithmetic for loop: for ((init; cond; incr))
387 if (cmd%num_tokens >= 2) then
388 if (index(cmd%tokens(2), '((') == 1) then
389 call process_for_arith_statement(cmd, shell)
390 return
391 end if
392 end if
393
394 ! Parse: for var [in word1 word2 ...]
395 ! POSIX: If 'in' is omitted, iterate over positional parameters ($@)
396 if (cmd%num_tokens < 2) then
397 write(error_unit, '(a)') 'for: syntax error, expected "for var"'
398 shell%last_exit_status = 1
399 return
400 end if
401
402 var_name = trim(cmd%tokens(2))
403
404 ! Check if 'in' is present
405 if (cmd%num_tokens >= 3 .and. trim(cmd%tokens(3)) == 'in') then
406 ! Standard form: for var in [items...]
407 ! If there are no items after "in", that's valid - just an empty loop
408 if (cmd%num_tokens < 4) then
409 final_count = 0
410 else
411 ! Process each token as a separate item (they're already tokenized correctly)
412 ! Expand braces and globs as needed
413 final_count = 0
414 do i = 4, cmd%num_tokens
415 ! Check if this token needs brace expansion
416 if (index(cmd%tokens(i), '{') > 0 .and. index(cmd%tokens(i), '}') > 0) then
417 ! Expand braces for this token (e.g., {1..5} -> 1 2 3 4 5)
418 call expand_braces(trim(cmd%tokens(i)), expanded_items, expanded_count)
419 ! Add all expanded items to final list
420 do j = 1, expanded_count
421 if (final_count < 100) then
422 final_count = final_count + 1
423 final_items(final_count) = trim(expanded_items(j))
424 end if
425 end do
426 ! Check if this token needs glob expansion
427 else if (has_unescaped_glob_chars(trim(cmd%tokens(i)))) then
428 ! Expand glob pattern (e.g., *.txt -> file1.txt file2.txt)
429 call glob_match(trim(cmd%tokens(i)), glob_matches, glob_count)
430 if (glob_count > 0) then
431 ! Add all matched files to final list
432 do j = 1, glob_count
433 if (final_count < 100) then
434 final_count = final_count + 1
435 final_items(final_count) = trim(glob_matches(j))
436 end if
437 end do
438 else
439 ! No matches - use pattern literally (POSIX behavior)
440 if (final_count < 100) then
441 final_count = final_count + 1
442 final_items(final_count) = trim(cmd%tokens(i))
443 end if
444 end if
445 else
446 ! No expansion needed - use token as-is (preserves quoted strings)
447 if (final_count < 100) then
448 final_count = final_count + 1
449 final_items(final_count) = trim(cmd%tokens(i))
450 end if
451 end if
452 end do
453 end if
454 else
455 ! No 'in' clause - POSIX: iterate over positional parameters ($@)
456 final_count = min(shell%num_positional, 100)
457 do i = 1, final_count
458 final_items(i) = trim(shell%positional_params(i)%str)
459 end do
460 end if
461
462 ! Push for block onto control stack with final split items
463 if (shell%control_depth < MAX_CONTROL_DEPTH) then
464 shell%control_depth = shell%control_depth + 1
465 shell%control_stack(shell%control_depth)%block_type = BLOCK_FOR
466 shell%control_stack(shell%control_depth)%loop_variable = var_name
467
468 ! Build list_part from final_items for storage (mainly for debugging)
469 list_part = ''
470 if (final_count > 0) then
471 do i = 1, final_count
472 if (len_trim(list_part) > 0) then
473 list_part = trim(list_part) // ' ' // trim(final_items(i))
474 else
475 list_part = trim(final_items(i))
476 end if
477 end do
478 end if
479 shell%control_stack(shell%control_depth)%for_list = list_part
480
481 ! Use final split items
482 shell%control_stack(shell%control_depth)%for_count = final_count
483 if (final_count > 0) then
484 allocate(shell%control_stack(shell%control_depth)%for_values(final_count))
485 do i = 1, final_count
486 shell%control_stack(shell%control_depth)%for_values(i) = trim(final_items(i))
487 end do
488 end if
489
490 ! Set up for first iteration - start at index 0 so first 'done' will set it to 1
491 shell%control_stack(shell%control_depth)%for_index = 0
492 if (shell%control_stack(shell%control_depth)%for_count > 0 .and. &
493 allocated(shell%control_stack(shell%control_depth)%for_values)) then
494 shell%control_stack(shell%control_depth)%should_execute = .true.
495 ! Don't set loop variable yet - let first 'done' do it
496 else
497 shell%control_stack(shell%control_depth)%should_execute = .false.
498 end if
499 else
500 write(error_unit, '(a)') 'Error: Control flow nesting too deep'
501 end if
502 end subroutine
503
504 ! Process arithmetic for loop: for ((init; condition; increment))
505 subroutine process_for_arith_statement(cmd, shell)
506 use expansion, only: arithmetic_expansion_shell
507 type(command_t), intent(in) :: cmd
508 type(shell_state_t), intent(inout) :: shell
509
510 character(len=512) :: full_expr, init_expr, cond_expr, incr_expr
511 integer :: i, start_pos, end_pos, semi1, semi2, paren_depth
512 character(len=:), allocatable :: result_value
513
514 ! Reconstruct the full (( ... )) expression from tokens
515 ! Check if token(1) contains the entire for((expression))
516 if (cmd%num_tokens >= 1 .and. len_trim(cmd%tokens(1)) >= 5) then
517 if (cmd%tokens(1)(1:5) == 'for((') then
518 ! Entire for(( expression is in token(1), strip the "for" prefix
519 full_expr = cmd%tokens(1)(4:) ! Start from position 4 to skip "for"
520 else
521 ! Tokens are separated: token(1)='for', token(2)='((...)'
522 full_expr = ''
523 do i = 2, cmd%num_tokens
524 if (len_trim(full_expr) > 0) then
525 full_expr = trim(full_expr) // ' ' // trim(cmd%tokens(i))
526 else
527 full_expr = trim(cmd%tokens(i))
528 end if
529 end do
530 end if
531 else
532 ! Tokens are separated or insufficient tokens
533 full_expr = ''
534 do i = 2, cmd%num_tokens
535 if (len_trim(full_expr) > 0) then
536 full_expr = trim(full_expr) // ' ' // trim(cmd%tokens(i))
537 else
538 full_expr = trim(cmd%tokens(i))
539 end if
540 end do
541 end if
542
543
544 ! Find the (( and )) boundaries
545 start_pos = index(full_expr, '((')
546 if (start_pos == 0) then
547 write(error_unit, '(a)') 'for: syntax error in arithmetic for loop'
548 shell%last_exit_status = 1
549 return
550 end if
551
552 ! Find matching ))
553 paren_depth = 0
554 end_pos = 0
555 i = start_pos
556 do while (i < len_trim(full_expr))
557 if (i+1 <= len_trim(full_expr) .and. full_expr(i:i+1) == '((') then
558 paren_depth = paren_depth + 1
559 i = i + 2
560 else if (i+1 <= len_trim(full_expr) .and. full_expr(i:i+1) == '))') then
561 paren_depth = paren_depth - 1
562 if (paren_depth == 0) then
563 end_pos = i
564 exit
565 end if
566 i = i + 2
567 else
568 i = i + 1
569 end if
570 end do
571
572 if (end_pos == 0) then
573 write(error_unit, '(a)') 'for: syntax error, unclosed (('
574 shell%last_exit_status = 1
575 return
576 end if
577
578 ! Extract content between (( and ))
579 full_expr = full_expr(start_pos+2:end_pos-1)
580
581 ! Split by semicolons to get init, condition, increment
582 semi1 = index(full_expr, ';')
583 if (semi1 > 0) then
584 init_expr = full_expr(:semi1-1)
585 semi2 = index(full_expr(semi1+1:), ';')
586 if (semi2 > 0) then
587 semi2 = semi1 + semi2
588 cond_expr = full_expr(semi1+1:semi2-1)
589 incr_expr = full_expr(semi2+1:)
590 else
591 ! Only one semicolon: init; condition (no increment)
592 cond_expr = full_expr(semi1+1:)
593 incr_expr = ''
594 end if
595 else
596 ! No semicolons: treat entire expression as condition
597 init_expr = ''
598 cond_expr = full_expr
599 incr_expr = ''
600 end if
601
602 ! Push arithmetic for block onto control stack
603 if (shell%control_depth < MAX_CONTROL_DEPTH) then
604 shell%control_depth = shell%control_depth + 1
605 shell%control_stack(shell%control_depth)%block_type = BLOCK_FOR_ARITH
606 shell%control_stack(shell%control_depth)%arith_init = trim(init_expr)
607 shell%control_stack(shell%control_depth)%arith_condition = trim(cond_expr)
608 shell%control_stack(shell%control_depth)%arith_increment = trim(incr_expr)
609 shell%control_stack(shell%control_depth)%arith_first_iteration = .true.
610 shell%control_stack(shell%control_depth)%should_execute = .true.
611
612 ! Execute initialization
613 if (len_trim(init_expr) > 0) then
614 result_value = arithmetic_expansion_shell('$((' // trim(init_expr) // '))', shell)
615 end if
616 else
617 write(error_unit, '(a)') 'Error: Control flow nesting too deep'
618 end if
619 end subroutine
620
621 subroutine process_then_statement(shell, should_execute)
622 type(shell_state_t), intent(inout) :: shell
623 logical, intent(out) :: should_execute
624
625 should_execute = .false. ! Don't execute the "then" keyword itself
626
627 if (shell%control_depth == 0) then
628 write(error_unit, '(a)') 'then: no matching if'
629 shell%last_exit_status = 1
630 return
631 end if
632
633 ! "then" marks the start of the if block
634 ! For single-line if statements, the remaining tokens after "then" will be
635 ! handled as separate commands by the main execution loop
636 end subroutine
637
638 subroutine process_else_statement(shell, should_execute)
639 type(shell_state_t), intent(inout) :: shell
640 logical, intent(out) :: should_execute
641
642 should_execute = .false. ! Don't execute the "else" keyword itself
643
644 if (shell%control_depth == 0 .or. &
645 shell%control_stack(shell%control_depth)%block_type /= BLOCK_IF) then
646 write(error_unit, '(a)') 'else: no matching if'
647 shell%last_exit_status = 1
648 return
649 end if
650
651 ! Switch to else branch - flip the execution logic
652 shell%control_stack(shell%control_depth)%in_else_branch = .true.
653 shell%control_stack(shell%control_depth)%should_execute = &
654 .not. shell%control_stack(shell%control_depth)%condition_met
655 end subroutine
656
657 subroutine process_elif_statement(cmd, shell, should_execute)
658 type(command_t), intent(in) :: cmd
659 type(shell_state_t), intent(inout) :: shell
660 logical, intent(out) :: should_execute
661 logical :: condition_result
662 integer :: i
663 character(len=:), allocatable :: condition_cmd
664
665 should_execute = .false. ! Don't execute the "elif" keyword itself
666
667 if (shell%control_depth == 0 .or. &
668 shell%control_stack(shell%control_depth)%block_type /= BLOCK_IF) then
669 write(error_unit, '(a)') 'elif: no matching if'
670 shell%last_exit_status = 1
671 return
672 end if
673
674 ! Parse elif condition: elif [ condition ] or elif command
675 if (cmd%num_tokens < 2) then
676 write(error_unit, '(a)') 'elif: missing condition'
677 shell%last_exit_status = 1
678 return
679 end if
680
681 ! Build condition command from tokens (skip "elif")
682 condition_cmd = ''
683 do i = 2, cmd%num_tokens
684 if (len_trim(condition_cmd) > 0) then
685 condition_cmd = trim(condition_cmd) // ' ' // trim(cmd%tokens(i))
686 else
687 condition_cmd = trim(cmd%tokens(i))
688 end if
689 end do
690
691 ! Only evaluate elif if previous conditions were false
692 if (.not. shell%control_stack(shell%control_depth)%condition_met) then
693 ! Evaluate condition
694 call evaluate_condition(condition_cmd, shell, condition_result)
695
696 ! Update control stack based on condition result
697 shell%control_stack(shell%control_depth)%condition_met = condition_result
698 shell%control_stack(shell%control_depth)%should_execute = condition_result
699 else
700 ! Previous condition was met, skip this elif
701 shell%control_stack(shell%control_depth)%should_execute = .false.
702 end if
703
704 ! POSIX: Reset exit status after evaluating control flow condition
705 ! The 'elif' keyword itself doesn't fail - it just sets up control state
706 shell%last_exit_status = 0
707 end subroutine
708
709 subroutine process_fi_statement(shell, should_execute)
710 type(shell_state_t), intent(inout) :: shell
711 logical, intent(out) :: should_execute
712
713 should_execute = .false. ! Don't execute the "fi" keyword itself
714
715 if (shell%control_depth == 0 .or. &
716 shell%control_stack(shell%control_depth)%block_type /= BLOCK_IF) then
717 write(error_unit, '(a)') 'fi: no matching if'
718 shell%last_exit_status = 1
719 return
720 end if
721
722 ! Pop the if block from the stack
723 call pop_control_block(shell)
724 end subroutine
725
726 subroutine process_do_statement(cmd, shell, should_execute)
727 type(command_t), intent(in) :: cmd
728 type(shell_state_t), intent(inout) :: shell
729 logical, intent(out) :: should_execute
730
731 character(len=:), allocatable :: remainder_cmd
732 integer :: i
733
734 should_execute = .false. ! Don't execute the "do" keyword itself
735
736 if (shell%control_depth == 0) then
737 write(error_unit, '(a)') 'do: no matching while/for'
738 shell%last_exit_status = 1
739 return
740 end if
741
742 ! "do" marks the start of the loop body - start capturing commands
743 if (.not. allocated(shell%control_stack(shell%control_depth)%loop_body)) then
744 allocate(shell%control_stack(shell%control_depth)%loop_body(100))
745 block
746 integer :: k
747 do k = 1, 100
748 shell%control_stack(shell%control_depth)%loop_body(k)%str = ''
749 end do
750 end block
751 end if
752
753 shell%control_stack(shell%control_depth)%loop_body_count = 0
754 shell%control_stack(shell%control_depth)%capturing_loop_body = .true.
755 shell%control_stack(shell%control_depth)%capture_nesting_depth = 0
756
757 ! Handle single-line loops: for x in a; do echo $x; done
758 ! If there are tokens after "do", capture them as the first loop body command
759 if (cmd%num_tokens > 1) then
760 ! Build command from remaining tokens (skip "do")
761 remainder_cmd = ''
762 do i = 2, cmd%num_tokens
763 if (len_trim(remainder_cmd) > 0) then
764 remainder_cmd = trim(remainder_cmd) // ' ' // trim(cmd%tokens(i))
765 else
766 remainder_cmd = trim(cmd%tokens(i))
767 end if
768 end do
769
770 ! Capture the command in the loop body
771 if (len_trim(remainder_cmd) > 0) then
772 call capture_loop_command(shell, trim(remainder_cmd))
773 end if
774 end if
775 end subroutine
776
777 subroutine process_done_statement(shell, should_execute)
778 use expansion, only: arithmetic_expansion_shell
779 type(shell_state_t), intent(inout) :: shell
780 logical, intent(out) :: should_execute
781
782 character(len=:), allocatable :: cond_result
783 integer :: cond_value
784 logical :: condition_result
785
786 should_execute = .false. ! Don't execute the "done" keyword itself
787
788 if (shell%control_depth == 0) then
789 ! Silently return if called when no loop is active (can happen during cleanup)
790 shell%last_exit_status = 0
791 return
792 end if
793
794 if (shell%control_stack(shell%control_depth)%block_type /= BLOCK_WHILE .and. &
795 shell%control_stack(shell%control_depth)%block_type /= BLOCK_UNTIL .and. &
796 shell%control_stack(shell%control_depth)%block_type /= BLOCK_FOR .and. &
797 shell%control_stack(shell%control_depth)%block_type /= BLOCK_FOR_ARITH) then
798 write(error_unit, '(a)') 'done: no matching while/for/until'
799 shell%last_exit_status = 1
800 return
801 end if
802
803 ! Stop capturing loop body
804 shell%control_stack(shell%control_depth)%capturing_loop_body = .false.
805
806 ! Check if break or continue was requested (for multi-level propagation)
807 if (shell%control_stack(shell%control_depth)%break_requested) then
808 ! Break requested - exit the loop
809 shell%control_stack(shell%control_depth)%break_requested = .false.
810 shell%control_stack(shell%control_depth)%break_level = 0
811 ! Pop the control stack
812 shell%control_depth = shell%control_depth - 1
813 shell%last_exit_status = 0
814 return
815 end if
816
817 if (shell%control_stack(shell%control_depth)%continue_requested) then
818 ! Continue requested - skip to next iteration
819 shell%control_stack(shell%control_depth)%continue_requested = .false.
820 shell%control_stack(shell%control_depth)%continue_level = 0
821 ! Don't return - fall through to iteration logic
822 end if
823
824 ! Handle for loop iteration
825 if (shell%control_stack(shell%control_depth)%block_type == BLOCK_FOR) then
826 ! Increment to next iteration value
827 shell%control_stack(shell%control_depth)%for_index = &
828 shell%control_stack(shell%control_depth)%for_index + 1
829
830 ! Check if we have more iterations to do
831 if (shell%control_stack(shell%control_depth)%for_index <= &
832 shell%control_stack(shell%control_depth)%for_count .and. &
833 allocated(shell%control_stack(shell%control_depth)%for_values)) then
834 ! Set variable to current iteration value
835 call set_shell_variable(shell, &
836 trim(shell%control_stack(shell%control_depth)%loop_variable), &
837 trim(shell%control_stack(shell%control_depth)%for_values(&
838 shell%control_stack(shell%control_depth)%for_index)))
839
840 ! Mark that we need to replay - executor will handle it
841 ! Don't pop the stack, just return - executor will see loop needs replay
842 return
843 end if
844
845
846 else if (shell%control_stack(shell%control_depth)%block_type == BLOCK_FOR_ARITH) then
847 ! Arithmetic for loop: execute increment, then check condition
848 ! Execute increment expression
849 if (len_trim(shell%control_stack(shell%control_depth)%arith_increment) > 0) then
850 cond_result = arithmetic_expansion_shell( &
851 '$((' // trim(shell%control_stack(shell%control_depth)%arith_increment) // '))', shell)
852 end if
853
854 ! Evaluate condition
855 if (len_trim(shell%control_stack(shell%control_depth)%arith_condition) > 0) then
856 cond_result = arithmetic_expansion_shell( &
857 '$((' // trim(shell%control_stack(shell%control_depth)%arith_condition) // '))', shell)
858
859 ! Check if condition is true (non-zero)
860 if (allocated(cond_result)) then
861 read(cond_result, *, iostat=cond_value) cond_value
862 if (cond_value == 0) cond_value = 0
863 else
864 cond_value = 0
865 end if
866
867 if (cond_value /= 0) then
868 ! Condition is true - continue loop, executor will replay
869 return
870 end if
871 else
872 ! No condition means infinite loop - but we'll exit for now
873 ! In a real implementation would loop back
874 end if
875
876 else if (shell%control_stack(shell%control_depth)%block_type == BLOCK_WHILE) then
877 ! For while loops, re-evaluate condition and replay if true
878 ! The condition is stored in condition_cmd
879
880 if (len_trim(shell%control_stack(shell%control_depth)%condition_cmd) > 0) then
881 ! Re-evaluate the while condition with current variable values
882 call evaluate_condition(shell%control_stack(shell%control_depth)%condition_cmd, &
883 shell, condition_result)
884
885 if (condition_result) then
886 ! Condition is still true - executor will replay the loop body
887 return
888 end if
889
890 ! POSIX: Reset exit status after loop condition check
891 ! The 'done' keyword itself doesn't fail - it just ends the loop
892 shell%last_exit_status = 0
893 end if
894
895 else if (shell%control_stack(shell%control_depth)%block_type == BLOCK_UNTIL) then
896 ! For until loops, re-evaluate condition and replay if FALSE (inverted logic)
897 ! The condition is stored in condition_cmd
898
899 if (len_trim(shell%control_stack(shell%control_depth)%condition_cmd) > 0) then
900 ! Re-evaluate the until condition with current variable values
901 if (associated(evaluate_condition)) then
902 call evaluate_condition(shell%control_stack(shell%control_depth)%condition_cmd, &
903 shell, condition_result)
904 else
905 write(error_unit, '(a)') 'ERROR: evaluate_condition is not initialized!'
906 condition_result = .false.
907 end if
908
909 ! until loops continue while condition is FALSE (opposite of while)
910 if (.not. condition_result) then
911 ! Condition is still false - executor will replay the loop body
912 ! POSIX: Reset exit status before returning
913 ! The 'done' keyword itself doesn't fail - it's checking loop continuation
914 shell%last_exit_status = 0
915 return
916 end if
917
918 ! POSIX: Reset exit status after loop condition check
919 ! The 'done' keyword itself doesn't fail - it just ends the loop
920 shell%last_exit_status = 0
921 end if
922 end if
923
924 ! Pop the loop block from the stack
925 call pop_control_block(shell)
926 end subroutine
927
928 subroutine push_control_block(shell, block_type, condition_met)
929 type(shell_state_t), intent(inout) :: shell
930 integer, intent(in) :: block_type
931 logical, intent(in) :: condition_met
932
933 if (shell%control_depth < MAX_CONTROL_DEPTH) then
934 shell%control_depth = shell%control_depth + 1
935 shell%control_stack(shell%control_depth)%block_type = block_type
936 shell%control_stack(shell%control_depth)%condition_met = condition_met
937 shell%control_stack(shell%control_depth)%in_else_branch = .false.
938 shell%control_stack(shell%control_depth)%should_execute = condition_met
939 else
940 write(error_unit, '(a)') 'Error: Control flow nesting too deep'
941 end if
942 end subroutine
943
944 subroutine pop_control_block(shell)
945 type(shell_state_t), intent(inout) :: shell
946
947 if (shell%control_depth > 0) then
948 ! Deallocate for_values if allocated
949 if (allocated(shell%control_stack(shell%control_depth)%for_values)) then
950 deallocate(shell%control_stack(shell%control_depth)%for_values)
951 end if
952 ! Clear loop body to prevent replay in subsequent loops
953 shell%control_stack(shell%control_depth)%loop_body_count = 0
954 shell%control_stack(shell%control_depth)%capturing_loop_body = .false.
955 shell%control_depth = shell%control_depth - 1
956 end if
957 end subroutine
958
959 function should_execute_command(shell) result(should_exec)
960 type(shell_state_t), intent(in) :: shell
961 logical :: should_exec
962 integer :: i
963
964 should_exec = .true.
965
966 ! Check all control blocks in the stack
967 do i = 1, shell%control_depth
968 if (.not. shell%control_stack(i)%should_execute) then
969 should_exec = .false.
970 exit
971 end if
972 end do
973 end function
974
975
976 ! Helper to tokenize and expand variables
977 subroutine tokenize_and_expand(input, tokens, num_tokens, shell)
978 character(len=*), intent(in) :: input
979 character(len=256), intent(out) :: tokens(:)
980 integer, intent(out) :: num_tokens
981 type(shell_state_t), intent(inout) :: shell
982
983 integer :: pos, start_pos
984 character(len=256) :: expanded_token
985 character(len=:), allocatable :: expanded_result
986
987 num_tokens = 0
988 start_pos = 1
989
990 do while (start_pos <= len_trim(input) .and. num_tokens < size(tokens))
991 ! Skip leading spaces
992 do while (start_pos <= len_trim(input) .and. input(start_pos:start_pos) == ' ')
993 start_pos = start_pos + 1
994 end do
995
996 if (start_pos > len_trim(input)) exit
997
998 ! Find end of token
999 pos = start_pos
1000 do while (pos <= len_trim(input) .and. input(pos:pos) /= ' ')
1001 pos = pos + 1
1002 end do
1003
1004 ! Extract token and expand variables
1005 num_tokens = num_tokens + 1
1006 expanded_token = input(start_pos:pos-1)
1007
1008 ! Expand variables in the token
1009 if (index(expanded_token, '$') > 0) then
1010 call simple_variable_expand(expanded_token, expanded_result, shell)
1011 if (allocated(expanded_result)) then
1012 tokens(num_tokens) = expanded_result
1013 else
1014 tokens(num_tokens) = expanded_token
1015 end if
1016 else
1017 tokens(num_tokens) = expanded_token
1018 end if
1019
1020 start_pos = pos + 1
1021 end do
1022 end subroutine
1023
1024 subroutine execute_test_condition(test_cmd, shell, result)
1025 use test_builtin, only: execute_test_command
1026 character(len=*), intent(in) :: test_cmd
1027 type(shell_state_t), intent(inout) :: shell
1028 logical, intent(out) :: result
1029
1030 type(command_t) :: cmd
1031 character(len=256) :: tokens(50), expanded_token
1032 integer :: num_tokens, i, pos, start_pos, test_exit_status
1033 character(len=:), allocatable :: trimmed_cmd
1034 character(len=:), allocatable :: expanded_result
1035 character(len=1) :: quote_char
1036
1037 ! Check if this is a [[ ]] expression
1038 trimmed_cmd = trim(test_cmd)
1039
1040 if (index(trimmed_cmd, '[[') > 0) then
1041 ! This is an advanced test expression [[ ... ]]
1042 ! Tokenize the expression
1043
1044 num_tokens = 0
1045 start_pos = 1
1046
1047 ! Quote-aware tokenization
1048 do while (start_pos <= len_trim(trimmed_cmd) .and. num_tokens < 50)
1049 ! Skip leading spaces
1050 do while (start_pos <= len_trim(trimmed_cmd) .and. trimmed_cmd(start_pos:start_pos) == ' ')
1051 start_pos = start_pos + 1
1052 end do
1053
1054 if (start_pos > len_trim(trimmed_cmd)) exit
1055
1056 ! Check if token starts with a quote
1057 if (trimmed_cmd(start_pos:start_pos) == '"' .or. trimmed_cmd(start_pos:start_pos) == "'") then
1058 ! Find matching closing quote
1059 quote_char = trimmed_cmd(start_pos:start_pos)
1060 pos = start_pos + 1
1061 do while (pos <= len_trim(trimmed_cmd))
1062 if (trimmed_cmd(pos:pos) == quote_char) then
1063 ! Check if escaped (preceded by backslash)
1064 if (pos > start_pos + 1 .and. trimmed_cmd(pos-1:pos-1) == '\') then
1065 pos = pos + 1
1066 cycle
1067 end if
1068 exit
1069 end if
1070 pos = pos + 1
1071 end do
1072 ! Extract quoted content (without the quotes)
1073 num_tokens = num_tokens + 1
1074 if (pos > start_pos + 1) then
1075 tokens(num_tokens) = trimmed_cmd(start_pos+1:pos-1)
1076 else
1077 tokens(num_tokens) = ''
1078 end if
1079 start_pos = pos + 1
1080 else
1081 ! Find end of token (space-delimited)
1082 pos = start_pos
1083 do while (pos <= len_trim(trimmed_cmd) .and. trimmed_cmd(pos:pos) /= ' ')
1084 pos = pos + 1
1085 end do
1086 ! Extract token
1087 num_tokens = num_tokens + 1
1088 tokens(num_tokens) = trimmed_cmd(start_pos:pos-1)
1089 start_pos = pos + 1
1090 end if
1091 end do
1092
1093 ! Call advanced test evaluator
1094 test_exit_status = evaluate_test_expression(shell, tokens, num_tokens)
1095 result = (test_exit_status == 0)
1096
1097 else
1098 ! For [ ] test commands, tokenize and call the test builtin
1099 num_tokens = 0
1100 start_pos = 1
1101
1102 do while (start_pos <= len_trim(trimmed_cmd) .and. num_tokens < 50)
1103 ! Skip leading spaces
1104 do while (start_pos <= len_trim(trimmed_cmd) .and. trimmed_cmd(start_pos:start_pos) == ' ')
1105 start_pos = start_pos + 1
1106 end do
1107
1108 if (start_pos > len_trim(trimmed_cmd)) exit
1109
1110 ! Find end of token
1111 pos = start_pos
1112 do while (pos <= len_trim(trimmed_cmd) .and. trimmed_cmd(pos:pos) /= ' ')
1113 pos = pos + 1
1114 end do
1115
1116 ! Extract token and expand variables
1117 num_tokens = num_tokens + 1
1118 expanded_token = trimmed_cmd(start_pos:pos-1)
1119
1120 ! Expand variables in the token (e.g., $count becomes the value of count)
1121 if (index(expanded_token, '$') > 0) then
1122 ! Use get_shell_variable for simple $var expansion
1123 ! parameter_expansion is only for ${var} format
1124 call simple_variable_expand(expanded_token, expanded_result, shell)
1125 if (allocated(expanded_result)) then
1126 tokens(num_tokens) = expanded_result
1127 else
1128 tokens(num_tokens) = expanded_token
1129 end if
1130 else
1131 tokens(num_tokens) = expanded_token
1132 end if
1133
1134 start_pos = pos + 1
1135 end do
1136
1137 ! If we have tokens and the first is '[' or 'test', execute the test builtin
1138 if (num_tokens > 0 .and. (trim(tokens(1)) == '[' .or. trim(tokens(1)) == 'test')) then
1139 ! Build command structure
1140 cmd%num_tokens = num_tokens
1141 allocate(character(len=256) :: cmd%tokens(num_tokens))
1142 do i = 1, num_tokens
1143 cmd%tokens(i) = trim(tokens(i))
1144 end do
1145
1146 ! Execute the test command
1147 call execute_test_command(cmd, shell)
1148
1149 ! Clean up
1150 deallocate(cmd%tokens)
1151
1152 ! Check exit status
1153 result = (shell%last_exit_status == 0)
1154 else
1155 ! Fallback: check last exit status
1156 result = (shell%last_exit_status == 0)
1157 end if
1158 end if
1159
1160 end subroutine
1161
1162 function count_substring(string, substring) result(count)
1163 character(len=*), intent(in) :: string, substring
1164 integer :: count, pos, start
1165
1166 count = 0
1167 start = 1
1168 do
1169 pos = index(string(start:), substring)
1170 if (pos == 0) exit
1171 count = count + 1
1172 start = start + pos + len(substring) - 1
1173 end do
1174 end function
1175
1176 subroutine parse_for_values(block, list_str)
1177 type(control_block_t), intent(inout) :: block
1178 character(len=*), intent(in) :: list_str
1179
1180 character(len=256) :: temp_values(20) ! Max 20 values
1181 integer :: count, start_pos, end_pos, i
1182
1183 count = 0
1184 start_pos = 1
1185
1186 ! Parse space-separated values
1187 do
1188 ! Skip leading spaces
1189 do while (start_pos <= len_trim(list_str) .and. list_str(start_pos:start_pos) == ' ')
1190 start_pos = start_pos + 1
1191 end do
1192
1193 if (start_pos > len_trim(list_str)) exit
1194
1195 ! Find end of current word
1196 end_pos = start_pos
1197 do while (end_pos <= len_trim(list_str) .and. list_str(end_pos:end_pos) /= ' ')
1198 end_pos = end_pos + 1
1199 end do
1200
1201 count = count + 1
1202 if (count <= 20) then
1203 temp_values(count) = list_str(start_pos:end_pos-1)
1204 end if
1205
1206 start_pos = end_pos + 1
1207 end do
1208
1209 block%for_count = count
1210 if (count > 0) then
1211 allocate(block%for_values(count))
1212 do i = 1, count
1213 block%for_values(i) = trim(temp_values(i))
1214 end do
1215 end if
1216 end subroutine
1217
1218 subroutine process_function_statement(cmd, shell, should_execute)
1219 type(command_t), intent(in) :: cmd
1220 type(shell_state_t), intent(inout) :: shell
1221 logical, intent(out) :: should_execute
1222
1223 should_execute = .false. ! Don't execute function definition itself
1224
1225 if (cmd%num_tokens < 2) then
1226 write(error_unit, '(a)') 'function: missing function name'
1227 shell%last_exit_status = 1
1228 return
1229 end if
1230
1231 write(output_unit, '(a)') 'function definitions not fully implemented yet'
1232 write(output_unit, '(a,a)') 'Would define function: ', trim(cmd%tokens(2))
1233 end subroutine
1234
1235 ! Note: return and local are now implemented as builtins in builtins.f90
1236 ! Note: set_shell_variable is now imported from the variables module
1237
1238 subroutine handle_case_statement(cmd, shell)
1239 type(command_t), intent(in) :: cmd
1240 type(shell_state_t), intent(inout) :: shell
1241
1242 character(len=256) :: case_variable, expanded_value
1243
1244 if (cmd%num_tokens < 3) then
1245 write(error_unit, '(a)') 'case: syntax error, expected "case variable in"'
1246 shell%last_exit_status = 1
1247 return
1248 end if
1249
1250 if (trim(cmd%tokens(3)) /= 'in' .and. cmd%num_tokens >= 3) then
1251 write(error_unit, '(a)') 'case: syntax error, expected "in" keyword'
1252 shell%last_exit_status = 1
1253 return
1254 end if
1255
1256 case_variable = trim(cmd%tokens(2))
1257
1258 ! Expand the variable to get its value
1259 call expand_case_variable(shell, case_variable, expanded_value)
1260
1261 ! Initialize case block
1262 if (shell%control_depth < MAX_CONTROL_DEPTH) then
1263 shell%control_depth = shell%control_depth + 1
1264 shell%control_stack(shell%control_depth)%block_type = BLOCK_CASE
1265 shell%control_stack(shell%control_depth)%condition_met = .false.
1266 shell%control_stack(shell%control_depth)%condition_cmd = expanded_value
1267 shell%control_stack(shell%control_depth)%loop_start_line = 0
1268 shell%control_stack(shell%control_depth)%case_found_match = .false.
1269 shell%control_stack(shell%control_depth)%case_in_match = .false.
1270 else
1271 write(error_unit, '(a)') 'case: control structure too deeply nested'
1272 shell%last_exit_status = 1
1273 end if
1274 end subroutine
1275
1276 subroutine handle_case_pattern(cmd, shell)
1277 type(command_t), intent(in) :: cmd
1278 type(shell_state_t), intent(inout) :: shell
1279
1280 character(len=:), allocatable :: case_value
1281 character(len=256) :: pattern
1282 logical :: pattern_matches
1283 integer :: i
1284
1285 if (shell%control_depth == 0) then
1286 write(error_unit, '(a)') 'case pattern outside case statement'
1287 shell%last_exit_status = 1
1288 return
1289 end if
1290
1291 if (shell%control_stack(shell%control_depth)%block_type /= BLOCK_CASE) then
1292 write(error_unit, '(a)') 'case pattern in wrong context'
1293 shell%last_exit_status = 1
1294 return
1295 end if
1296
1297 ! If we've already found a match, skip all subsequent patterns
1298 if (shell%control_stack(shell%control_depth)%case_found_match) then
1299 shell%control_stack(shell%control_depth)%condition_met = .false.
1300 shell%control_stack(shell%control_depth)%case_in_match = .false.
1301 return
1302 end if
1303
1304 ! Get the case value we're matching against
1305 case_value = shell%control_stack(shell%control_depth)%condition_cmd
1306
1307 ! Check if any pattern matches - patterns end with )
1308 pattern_matches = .false.
1309 do i = 1, cmd%num_tokens
1310 if (index(cmd%tokens(i), ')') > 0) then
1311 ! Remove the ) from pattern
1312 pattern = cmd%tokens(i)
1313 if (len_trim(pattern) > 0 .and. pattern(len_trim(pattern):len_trim(pattern)) == ')') then
1314 pattern = pattern(1:len_trim(pattern)-1)
1315 end if
1316
1317 ! Check for multi-pattern (e.g., a|b|c)
1318 ! Split on | and check each sub-pattern
1319 call check_multi_pattern(case_value, pattern, pattern_matches)
1320
1321 if (pattern_matches) then
1322 exit
1323 end if
1324 end if
1325 end do
1326
1327 ! Set condition based on pattern match
1328 shell%control_stack(shell%control_depth)%condition_met = pattern_matches
1329 shell%control_stack(shell%control_depth)%case_in_match = pattern_matches
1330 if (pattern_matches) then
1331 shell%control_stack(shell%control_depth)%case_found_match = .true.
1332 end if
1333 end subroutine
1334
1335 subroutine handle_esac_statement(shell)
1336 type(shell_state_t), intent(inout) :: shell
1337
1338 if (shell%control_depth == 0) then
1339 write(error_unit, '(a)') 'esac without matching case'
1340 shell%last_exit_status = 1
1341 return
1342 end if
1343
1344 if (shell%control_stack(shell%control_depth)%block_type /= BLOCK_CASE) then
1345 write(error_unit, '(a)') 'esac without matching case'
1346 shell%last_exit_status = 1
1347 return
1348 end if
1349
1350 ! Pop case block from stack
1351 shell%control_depth = shell%control_depth - 1
1352 shell%last_exit_status = 0
1353 end subroutine
1354
1355 subroutine expand_case_variable(shell, variable_name, expanded_value)
1356 type(shell_state_t), intent(in) :: shell
1357 character(len=*), intent(in) :: variable_name
1358 character(len=*), intent(out) :: expanded_value
1359
1360 integer :: i
1361
1362 expanded_value = ''
1363
1364 ! Simple variable expansion
1365 if (variable_name(1:1) == '$') then
1366 ! Variable reference
1367 do i = 1, shell%num_variables
1368 if (trim(shell%variables(i)%name) == trim(variable_name(2:))) then
1369 expanded_value = trim(shell%variables(i)%value)
1370 return
1371 end if
1372 end do
1373 ! Variable not found - leave empty
1374 expanded_value = ''
1375 else
1376 ! Not a variable reference - use the literal value
1377 expanded_value = trim(variable_name)
1378 end if
1379 end subroutine
1380
1381 function case_pattern_match(value, pattern) result(matches)
1382 character(len=*), intent(in) :: value, pattern
1383 logical :: matches
1384
1385 ! Use the pattern_matches_no_dotfile_check function from glob module
1386 ! This handles *, ?, [abc], [!abc], [[:class:]], etc. correctly
1387 ! without the dotfile exclusion (which shouldn't apply to case statements)
1388 ! Note: Don't trim value - it might BE whitespace (e.g., matching " " against [[:space:]])
1389 matches = pattern_matches_no_dotfile_check(trim(pattern), value)
1390 end function
1391
1392 ! Check multi-pattern (e.g., a|b|c) - split on | and check each
1393 subroutine check_multi_pattern(value, pattern_str, matches)
1394 character(len=*), intent(in) :: value, pattern_str
1395 logical, intent(out) :: matches
1396
1397 character(len=256) :: sub_patterns(20)
1398 integer :: num_patterns, i, pipe_pos
1399 character(len=256) :: remaining
1400
1401 matches = .false.
1402
1403 ! Check if pattern contains | (multi-pattern)
1404 if (index(pattern_str, '|') == 0) then
1405 ! Single pattern, just match directly
1406 matches = case_pattern_match(value, pattern_str)
1407 return
1408 end if
1409
1410 ! Split on | to get individual patterns
1411 num_patterns = 0
1412 remaining = trim(pattern_str)
1413
1414 do while (len_trim(remaining) > 0 .and. num_patterns < 20)
1415 pipe_pos = index(remaining, '|')
1416 if (pipe_pos > 0) then
1417 ! Found a |, extract pattern before it
1418 num_patterns = num_patterns + 1
1419 sub_patterns(num_patterns) = remaining(1:pipe_pos-1)
1420 remaining = remaining(pipe_pos+1:)
1421 else
1422 ! No more |, this is the last pattern
1423 num_patterns = num_patterns + 1
1424 sub_patterns(num_patterns) = remaining
1425 exit
1426 end if
1427 end do
1428
1429 ! Check each sub-pattern
1430 do i = 1, num_patterns
1431 if (case_pattern_match(value, trim(sub_patterns(i)))) then
1432 matches = .true.
1433 return
1434 end if
1435 end do
1436 end subroutine
1437
1438 ! Capture a command into the loop body buffer
1439 subroutine capture_loop_command(shell, command_line)
1440 type(shell_state_t), intent(inout) :: shell
1441 character(len=*), intent(in) :: command_line
1442
1443 if (shell%control_depth == 0) return
1444 if (.not. shell%control_stack(shell%control_depth)%capturing_loop_body) return
1445
1446 ! Add command to buffer
1447 shell%control_stack(shell%control_depth)%loop_body_count = &
1448 shell%control_stack(shell%control_depth)%loop_body_count + 1
1449
1450 if (shell%control_stack(shell%control_depth)%loop_body_count <= 100) then
1451 shell%control_stack(shell%control_depth)%loop_body(&
1452 shell%control_stack(shell%control_depth)%loop_body_count)%str = command_line
1453 end if
1454 end subroutine
1455
1456 ! Check if loop body replay is needed
1457 function should_replay_loop(shell) result(should_replay)
1458 type(shell_state_t), intent(in) :: shell
1459 logical :: should_replay
1460
1461 should_replay = .false.
1462 if (shell%control_depth == 0) return
1463 if (shell%control_stack(shell%control_depth)%loop_body_count == 0) return
1464
1465 should_replay = .true.
1466 end function
1467
1468 ! Simple variable expansion for $var (not ${var})
1469 subroutine simple_variable_expand(input, output, shell)
1470 character(len=*), intent(in) :: input
1471 character(len=:), allocatable, intent(out) :: output
1472 type(shell_state_t), intent(inout) :: shell
1473 character(len=4096) :: result
1474 character(len=256) :: var_name
1475 character(len=:), allocatable :: var_value
1476 integer :: i, j, var_start
1477
1478 result = ''
1479 i = 1
1480 j = 1
1481
1482 do while (i <= len_trim(input))
1483 if (input(i:i) == '$' .and. i < len_trim(input)) then
1484 i = i + 1
1485 var_start = i
1486
1487 ! Extract variable name (alphanumeric + underscore)
1488 do while (i <= len_trim(input))
1489 if (.not. ((input(i:i) >= 'a' .and. input(i:i) <= 'z') .or. &
1490 (input(i:i) >= 'A' .and. input(i:i) <= 'Z') .or. &
1491 (input(i:i) >= '0' .and. input(i:i) <= '9') .or. &
1492 input(i:i) == '_')) exit
1493 i = i + 1
1494 end do
1495
1496 if (i > var_start) then
1497 var_name = input(var_start:i-1)
1498 var_value = get_shell_variable(shell, trim(var_name))
1499 if (len_trim(var_value) > 0) then
1500 result(j:j+len_trim(var_value)-1) = trim(var_value)
1501 j = j + len_trim(var_value)
1502 end if
1503 else
1504 ! Just a $ with no variable name
1505 result(j:j) = '$'
1506 j = j + 1
1507 end if
1508 else
1509 result(j:j) = input(i:i)
1510 i = i + 1
1511 j = j + 1
1512 end if
1513 end do
1514
1515 output = trim(result)
1516 end subroutine
1517
1518 end module control_flow