Fortran · 141262 bytes Raw Blame History
1 ! =====================================
2 ! AST Executor Module - Execute parsed command trees
3 ! =====================================
4 ! Executes commands from the AST produced by the grammar parser
5 ! Part of the parser rewrite project
6 !
7 ! Status: PHASE 4 - AST execution implementation
8 ! Author: Parser Rewrite Team
9 ! Created: 2025-11-06
10
11 module ast_executor
12 use iso_fortran_env
13 use iso_c_binding
14 use shell_types
15 use command_tree
16 use system_interface
17 use job_control
18 use glob, only: pattern_matches_no_dotfile_check
19 use shell_options, only: check_errexit, trace_command
20 use io_helpers, only: write_stderr
21 implicit none
22 private
23
24 ! Public interface
25 public :: execute_ast
26 public :: execute_ast_node
27 public :: unset_ast_function
28 public :: is_ast_function
29 public :: execute_external_command ! Currently unused but may be needed later
30 public :: register_trap_evaluator
31
32 ! C bindings for process control
33 interface
34 function close(fd) bind(c, name='close')
35 import :: c_int
36 integer(c_int), value :: fd
37 integer(c_int) :: close
38 end function close
39 end interface
40
41 ! Global function AST cache (maps function name to AST body)
42 type :: function_ast_entry_t
43 character(len=256) :: name
44 type(command_node_t), pointer :: body => null()
45 end type function_ast_entry_t
46
47 type(function_ast_entry_t), save :: function_ast_cache(MAX_FUNCTIONS)
48 integer, save :: num_cached_functions = 0
49
50 contains
51
52 ! =====================================
53 ! Main Entry Point
54 ! =====================================
55
56 ! Execute an AST and return exit status
57 function execute_ast(root, shell) result(exit_status)
58 type(command_node_t), pointer, intent(in) :: root
59 type(shell_state_t), intent(inout) :: shell
60 integer :: exit_status
61
62 if (.not. associated(root)) then
63 exit_status = 0
64 return
65 end if
66
67 exit_status = execute_ast_node(root, shell)
68 end function execute_ast
69
70 ! Register the AST evaluator with trap_dispatch (breaks circular dep with executor)
71 subroutine register_trap_evaluator()
72 use trap_dispatch, only: set_trap_evaluator
73 call set_trap_evaluator(ast_eval_string)
74 end subroutine register_trap_evaluator
75
76 ! Wrapper matching trap_dispatch interface: parse string and execute via AST
77 subroutine ast_eval_string(cmd_string, shell, exit_code)
78 use grammar_parser, only: parse_command_line
79 character(len=*), intent(in) :: cmd_string
80 type(shell_state_t), intent(inout) :: shell
81 integer, intent(out) :: exit_code
82 type(command_node_t), pointer :: ast_root
83
84 exit_code = 0
85 ast_root => parse_command_line(cmd_string)
86 if (associated(ast_root)) then
87 exit_code = execute_ast(ast_root, shell)
88 end if
89 end subroutine ast_eval_string
90
91 ! =====================================
92 ! Node Execution Functions
93 ! =====================================
94
95 recursive function execute_ast_node(node, shell) result(exit_status)
96 type(command_node_t), pointer, intent(in) :: node
97 type(shell_state_t), intent(inout) :: shell
98 integer :: exit_status
99
100 if (.not. associated(node)) then
101 exit_status = 0
102 return
103 end if
104
105 ! Update LINENO to reflect current line being executed
106 if (node%line > 0) then
107 shell%current_line_number = node%line
108 end if
109
110 select case(node%node_type)
111 case(CMD_SIMPLE)
112 exit_status = execute_simple_command(node, shell)
113 case(CMD_PIPELINE)
114 exit_status = execute_pipeline_node(node, shell)
115 case(CMD_LIST)
116 exit_status = execute_list_node(node, shell)
117 case(CMD_IF_STATEMENT)
118 exit_status = execute_if_node(node, shell)
119 case(CMD_WHILE_LOOP, CMD_UNTIL_LOOP)
120 exit_status = execute_while_node(node, shell)
121 case(CMD_FOR_LOOP)
122 exit_status = execute_for_node(node, shell)
123 case(CMD_CASE_STATEMENT)
124 exit_status = execute_case_node(node, shell)
125 case(CMD_SUBSHELL)
126 exit_status = execute_subshell_node(node, shell)
127 case(CMD_BRACE_GROUP)
128 exit_status = execute_brace_group_node(node, shell)
129 case(CMD_FUNCTION_DEF)
130 ! Function definitions: Store in shell state
131 exit_status = execute_function_def(node, shell)
132 case(CMD_FOR_ARITH)
133 exit_status = execute_for_arith_node(node, shell)
134 case(CMD_COPROC)
135 exit_status = execute_coproc_node(node, shell)
136 case default
137 ! node_type = 0 usually means uninitialized/invalid AST node from parser
138 if (node%node_type == 0) then
139 call write_stderr('sh: -c: line 1: syntax error: unexpected end of file')
140 else
141 block
142 character(len=32) :: nt_str
143 write(nt_str, '(i0)') node%node_type
144 call write_stderr('fortsh: unknown node type: ' // trim(nt_str))
145 end block
146 end if
147 exit_status = 1
148 end select
149
150 shell%last_exit_status = exit_status
151 end function execute_ast_node
152
153 ! =====================================
154 ! Simple Command Execution
155 ! =====================================
156
157 function execute_simple_command(node, shell) result(exit_status)
158 use executor, only: execute_pipeline
159 use fd_redirection, only: apply_single_redirection, &
160 save_fd_mark, restore_fds_to_mark
161 use parser, only: expand_variables
162 use iso_fortran_env, only: error_unit
163 type(command_node_t), pointer, intent(in) :: node
164 type(shell_state_t), intent(inout) :: shell
165 integer :: exit_status
166 integer :: i, func_idx, old_num_positional, eq_pos, fd_mark
167 type(pipeline_t) :: temp_pipeline
168 type(redirection_t) :: temp_redirect
169 character(len=256) :: cmd_name ! Command names are short; was MAX_TOKEN_LEN (4096)
170 type(string_t), allocatable :: old_params(:)
171 character(len=:), allocatable :: expanded_filename
172 logical :: redir_success
173 logical :: has_redirects, is_pure_assignment
174
175 exit_status = 0
176
177 if (.not. associated(node%simple_cmd)) then
178 exit_status = 0
179 return
180 end if
181
182 ! Check for empty command (e.g., from empty command substitution result like $(true))
183 ! When a command substitution returns empty and is the only word, we have num_words=1
184 ! but the word itself is empty
185 if (node%simple_cmd%num_words > 0 .and. allocated(node%simple_cmd%words)) then
186 block
187 logical :: is_empty_cmd
188 integer :: check_i, check_len
189
190 ! Check if first word is empty (accounting for sentinel characters)
191 is_empty_cmd = .false.
192 check_len = len_trim(node%simple_cmd%words(1))
193 if (check_len == 0) then
194 is_empty_cmd = .true.
195 else
196 ! Check if word contains only sentinel characters (char(2), char(3))
197 is_empty_cmd = .true.
198 do check_i = 1, check_len
199 if (node%simple_cmd%words(1)(check_i:check_i) /= char(2) .and. &
200 node%simple_cmd%words(1)(check_i:check_i) /= char(3)) then
201 is_empty_cmd = .false.
202 exit
203 end if
204 end do
205 end if
206
207 if (is_empty_cmd) then
208 ! Empty first word - check if it was a quoted literal empty string
209 ! If so, it's an explicit empty command name which is "command not found"
210 ! If not quoted (came from expansion), just preserve exit status
211 if (allocated(node%simple_cmd%word_was_quoted)) then
212 if (node%simple_cmd%word_was_quoted(1)) then
213 ! Explicit empty string like '' or "" - command not found
214 ! Apply any redirections first (e.g., 2>/dev/null)
215 if (node%simple_cmd%num_redirects > 0) then
216 fd_mark = save_fd_mark()
217 block
218 use fd_redirection, only: apply_single_redirection
219 use parser, only: expand_variables
220 type(redirection_t) :: temp_redirect
221 logical :: redir_success
222 character(len=:), allocatable :: expanded_filename
223 integer :: redir_idx
224
225 do redir_idx = 1, node%simple_cmd%num_redirects
226 temp_redirect%type = node%simple_cmd%redirects(redir_idx)%type
227 temp_redirect%fd = node%simple_cmd%redirects(redir_idx)%fd
228 temp_redirect%target_fd = node%simple_cmd%redirects(redir_idx)%target_fd
229 if (allocated(node%simple_cmd%redirects(redir_idx)%filename)) then
230 call expand_variables(trim(node%simple_cmd%redirects(redir_idx)%filename), expanded_filename, shell)
231 if (allocated(expanded_filename)) then
232 temp_redirect%filename = expanded_filename
233 else
234 temp_redirect%filename = trim(node%simple_cmd%redirects(redir_idx)%filename)
235 end if
236 else
237 temp_redirect%filename = ''
238 end if
239 temp_redirect%force_clobber = node%simple_cmd%redirects(redir_idx)%force_clobber
240 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
241 end do
242 end block
243 end if
244 call write_stderr('fortsh: : command not found')
245 ! Restore file descriptors
246 if (node%simple_cmd%num_redirects > 0) then
247 call restore_fds_to_mark(fd_mark)
248 end if
249 exit_status = 127
250 shell%last_exit_status = exit_status
251 return
252 end if
253 end if
254 ! Empty from expansion - preserve exit status and return
255 exit_status = shell%last_exit_status
256 return
257 end if
258 end block
259 end if
260
261 if (node%simple_cmd%num_words == 0) then
262 ! Handle pure assignments (no command, just VAR=value)
263 if (node%simple_cmd%num_assignments > 0) then
264 ! Process assignments as shell variable settings
265 block
266 use variables, only: set_shell_variable
267 use parser, only: expand_variables
268 integer :: assign_idx, assign_eq_pos, value_len, token_len, saved_status
269 character(len=256) :: assign_name
270 character(len=:), allocatable :: assign_value, expanded_value
271
272 ! Save the exit status before assignment expansion
273 ! POSIX: Pure assignment exit status should be 0 unless command substitution fails
274 saved_status = shell%last_exit_status
275 shell%last_exit_status = 0
276
277 do assign_idx = 1, node%simple_cmd%num_assignments
278 ! Use tracked length to preserve trailing whitespace
279 if (allocated(node%simple_cmd%assignment_lengths) .and. &
280 assign_idx <= size(node%simple_cmd%assignment_lengths)) then
281 token_len = node%simple_cmd%assignment_lengths(assign_idx)
282 else
283 token_len = len_trim(node%simple_cmd%assignments(assign_idx))
284 end if
285 assign_eq_pos = index(node%simple_cmd%assignments(assign_idx), '=')
286 if (assign_eq_pos > 1) then
287 assign_name = node%simple_cmd%assignments(assign_idx)(1:assign_eq_pos-1)
288 block
289 use variables, only: safe_assign_alloc_str
290 integer :: av_len, av_full_len
291 ! Use assignment_lengths if available (preserves trailing whitespace like IFS=" ")
292 if (allocated(node%simple_cmd%assignment_lengths) .and. &
293 assign_idx <= size(node%simple_cmd%assignment_lengths)) then
294 av_full_len = node%simple_cmd%assignment_lengths(assign_idx)
295 else
296 av_full_len = len_trim(node%simple_cmd%assignments(assign_idx))
297 end if
298 av_len = av_full_len - assign_eq_pos
299 if (av_len < 0) av_len = 0
300 call safe_assign_alloc_str(assign_value, &
301 node%simple_cmd%assignments(assign_idx)(assign_eq_pos+1:), av_len)
302 end block
303
304 ! Handle array element assignment: VAR[key]=value or VAR[idx]=value
305 block
306 use variables, only: is_associative_array, set_assoc_array_value, &
307 set_array_element, strip_quotes
308 integer :: ab_pos, ab_end, ab_idx_status, ab_array_index
309 character(len=256) :: ab_base_name, ab_index_str
310
311 ab_pos = index(assign_name, '[')
312 if (ab_pos > 0) then
313 ab_end = index(assign_name(ab_pos:), ']')
314 if (ab_end > 0) then
315 ab_end = ab_pos + ab_end - 1
316 ab_base_name = assign_name(1:ab_pos-1)
317 ab_index_str = assign_name(ab_pos+1:ab_end-1)
318 call strip_quotes(ab_index_str)
319 ! Strip lexer sentinel chars (char(1)/char(2) inserted at quote boundaries)
320 block
321 integer :: si, so
322 character(len=256) :: cleaned_key
323 cleaned_key = ''
324 so = 0
325 do si = 1, len_trim(ab_index_str)
326 if (ab_index_str(si:si) /= char(1) .and. ab_index_str(si:si) /= char(2)) then
327 so = so + 1
328 cleaned_key(so:so) = ab_index_str(si:si)
329 end if
330 end do
331 ab_index_str = cleaned_key
332 end block
333
334 ! Calculate value_len for the value portion
335 if (len_trim(assign_value) > 0) then
336 value_len = len_trim(assign_value)
337 else
338 value_len = token_len - assign_eq_pos
339 if (value_len >= 2) then
340 if (node%simple_cmd%assignments(assign_idx)(assign_eq_pos+1:assign_eq_pos+1) == "'" .or. &
341 node%simple_cmd%assignments(assign_idx)(assign_eq_pos+1:assign_eq_pos+1) == '"') then
342 value_len = value_len - 2
343 end if
344 end if
345 if (value_len <= 0) value_len = 0
346 end if
347
348 ! Expand variables in value if needed
349 ! Use expanded_value directly to avoid truncation in fixed-size assign_value
350 if (index(assign_value, '$') > 0 .or. index(assign_value, '~') > 0) then
351 call expand_variables(assign_value, expanded_value, shell)
352 end if
353
354 if (is_associative_array(shell, trim(ab_base_name))) then
355 if (allocated(expanded_value)) then
356 call set_assoc_array_value(shell, trim(ab_base_name), &
357 trim(ab_index_str), expanded_value)
358 deallocate(expanded_value)
359 else
360 call set_assoc_array_value(shell, trim(ab_base_name), &
361 trim(ab_index_str), assign_value(1:value_len))
362 end if
363 else
364 read(ab_index_str, *, iostat=ab_idx_status) ab_array_index
365 if (ab_idx_status == 0) then
366 ab_array_index = ab_array_index + 1 ! Convert to 1-indexed
367 if (allocated(expanded_value)) then
368 call set_array_element(shell, trim(ab_base_name), &
369 ab_array_index, expanded_value)
370 deallocate(expanded_value)
371 else
372 call set_array_element(shell, trim(ab_base_name), &
373 ab_array_index, assign_value(1:value_len))
374 end if
375 else
376 write(error_unit, '(a)') 'Error: invalid array index'
377 shell%last_exit_status = 1
378 end if
379 end if
380 cycle ! Skip normal assignment processing
381 end if
382 end if
383 end block
384
385 ! Calculate value_len - normally use len_trim, but preserve
386 ! whitespace-only values (like IFS=" ")
387 if (len_trim(assign_value) > 0) then
388 ! Normal case: value has non-whitespace content
389 value_len = len_trim(assign_value)
390 else
391 ! Special case: value is empty or all whitespace
392 ! Use tracked length minus equals sign position
393 value_len = token_len - assign_eq_pos
394 ! Adjust for quotes if present in original
395 if (value_len >= 2) then
396 if (node%simple_cmd%assignments(assign_idx)(assign_eq_pos+1:assign_eq_pos+1) == "'" .or. &
397 node%simple_cmd%assignments(assign_idx)(assign_eq_pos+1:assign_eq_pos+1) == '"') then
398 value_len = value_len - 2 ! Remove quote characters from length
399 end if
400 end if
401 if (value_len <= 0) value_len = 0
402 end if
403
404 ! Check if this is an array assignment: VAR=(...)
405 if (value_len >= 2 .and. assign_value(1:1) == '(' .and. &
406 assign_value(value_len:value_len) == ')') then
407 ! Array assignment - expand variables/command substitutions first
408 block
409 use variables, only: handle_array_assignment
410 character(len=:), allocatable :: arr_expanded, arr_buf
411 if (index(assign_value(1:value_len), '$') > 0 &
412 .or. index(assign_value(1:value_len), &
413 '`') > 0) then
414 ! Expand content inside parens, then re-wrap
415 call expand_variables( &
416 assign_value(2:value_len-1), &
417 arr_expanded, shell)
418 if (allocated(arr_expanded)) then
419 arr_buf = '(' // arr_expanded // ')'
420 call handle_array_assignment(shell, &
421 trim(assign_name), &
422 arr_buf)
423 else
424 call handle_array_assignment(shell, &
425 trim(assign_name), &
426 assign_value(1:value_len))
427 end if
428 else
429 call handle_array_assignment(shell, &
430 trim(assign_name), &
431 assign_value(1:value_len))
432 end if
433 end block
434 ! Expand variables and command substitutions in the value
435 else if (index(assign_value, '$') > 0 .or. index(assign_value, '~') > 0) then
436 call expand_variables(assign_value, expanded_value, shell)
437 if (allocated(expanded_value)) then
438 call set_shell_variable(shell, trim(assign_name), expanded_value, len(expanded_value))
439 else
440 call set_shell_variable(shell, trim(assign_name), '', 0)
441 end if
442 else
443 ! Preserve whitespace in value by passing explicit length
444 ! Strip sentinel characters that may be embedded
445 block
446 character(len=:), allocatable :: clean_value
447 integer :: src_i, dst_i
448 logical :: is_int_var
449 ! Use a fixed-size stack buffer for sentinel stripping — avoids
450 ! allocatable assignment (clean_value='') which corrupts on flang-new.
451 ! Safe because value_len <= MAX_TOKEN_LEN and typical values are short.
452 allocate(character(len=max(value_len, 1)) :: clean_value)
453 dst_i = 1
454 do src_i = 1, value_len
455 if (assign_value(src_i:src_i) /= char(2) .and. &
456 assign_value(src_i:src_i) /= char(3) &
457 .and. &
458 assign_value(src_i:src_i) /= char(1)) &
459 then
460 clean_value(dst_i:dst_i) = &
461 assign_value(src_i:src_i)
462 dst_i = dst_i + 1
463 end if
464 end do
465 ! Check integer attribute
466 is_int_var = .false.
467 do src_i = 1, shell%num_variables
468 if (trim(shell%variables(src_i)%name) &
469 == trim(assign_name)) then
470 is_int_var = &
471 shell%variables(src_i)%is_integer
472 exit
473 end if
474 end do
475 if (is_int_var .and. dst_i > 1) then
476 block
477 use expansion, only: &
478 arithmetic_expansion_shell
479 character(len=:), allocatable :: ae, ar
480 ae = '$((' // &
481 clean_value(1:dst_i-1) // '))'
482 ar = arithmetic_expansion_shell( &
483 trim(ae), shell)
484 block
485 use variables, only: safe_assign_alloc_str
486 call safe_assign_alloc_str(clean_value, ar, len_trim(ar))
487 end block
488 dst_i = len_trim(ar) + 1
489 end block
490 end if
491 call set_shell_variable(shell, &
492 trim(assign_name), clean_value, &
493 dst_i - 1)
494 end block
495 end if
496
497 ! If allexport is enabled (set -a), automatically export the variable
498 if (shell%option_allexport) then
499 block
500 integer :: var_idx
501 do var_idx = 1, shell%num_variables
502 if (trim(shell%variables(var_idx)%name) == trim(assign_name)) then
503 shell%variables(var_idx)%exported = .true.
504 ! Also set in environment
505 if (.not. set_environment_var(trim(assign_name), trim(shell%variables(var_idx)%value))) then
506 ! Silently ignore export errors (POSIX behavior)
507 end if
508 exit
509 end if
510 end do
511 end block
512 end if
513 end if
514 end do
515 end block
516 end if
517 ! POSIX: Exit status of assignment is exit status of last command substitution
518 ! This covers readonly violations (127) and failed command substitutions
519 exit_status = shell%last_exit_status
520 call check_errexit(shell, exit_status)
521 return
522 end if
523
524 ! Special handling for exec with redirections
525 if (node%simple_cmd%num_words >= 1 .and. trim(node%simple_cmd%words(1)) == 'exec') then
526 ! Check if this is exec with only redirections (no command to execute)
527 if (node%simple_cmd%num_words == 1 .and. node%simple_cmd%num_redirects > 0) then
528 ! exec without arguments but with redirections - apply redirections permanently
529 block
530 use parser, only: expand_variables
531 character(len=:), allocatable :: expanded_filename
532 do i = 1, node%simple_cmd%num_redirects
533 ! Convert AST redirection to fd_redirection format
534 temp_redirect%type = node%simple_cmd%redirects(i)%type
535 temp_redirect%fd = node%simple_cmd%redirects(i)%fd
536 temp_redirect%target_fd = node%simple_cmd%redirects(i)%target_fd
537 if (allocated(node%simple_cmd%redirects(i)%filename)) then
538 ! Expand variables in the filename (e.g., $$ -> PID)
539 call expand_variables(trim(node%simple_cmd%redirects(i)%filename), expanded_filename, shell)
540 if (allocated(expanded_filename)) then
541 temp_redirect%filename = expanded_filename
542 else
543 temp_redirect%filename = trim(node%simple_cmd%redirects(i)%filename)
544 end if
545 else
546 temp_redirect%filename = ''
547 end if
548 temp_redirect%force_clobber = node%simple_cmd%redirects(i)%force_clobber
549
550 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber, permanent=.true.)
551 if (.not. redir_success) then
552 exit_status = 1
553 return
554 end if
555 end do
556 end block
557 exit_status = 0
558 return
559 else if (node%simple_cmd%num_words == 1) then
560 ! exec without arguments or redirections - just return success
561 exit_status = 0
562 return
563 end if
564 ! If we get here, exec has arguments, so fall through to normal execution
565 end if
566
567 ! Check for alias expansion (before function/builtin/external lookup)
568 ! Aliases only expand in interactive mode or with shopt expand_aliases
569 if (node%simple_cmd%num_words >= 1 .and. &
570 (shell%is_interactive .or. shell%shopt_expand_aliases) .and. &
571 .not. shell%bypass_aliases .and. .not. shell%executing_trap) then
572 block
573 character(len=256) :: first_word
574 integer :: alias_i
575 first_word = trim(node%simple_cmd%words(1))
576 do alias_i = 1, shell%num_aliases
577 if (trim(shell%aliases(alias_i)%name) == trim(first_word)) then
578 ! Found alias — rebuild command line with expansion and re-parse
579 block
580 use grammar_parser, only: parse_command_line
581 use command_tree, only: destroy_command_node
582 character(len=:), allocatable :: expanded_line
583 type(command_node_t), pointer :: alias_ast
584 integer :: word_i
585 expanded_line = trim(shell%aliases(alias_i)%command)
586 ! Append remaining arguments
587 do word_i = 2, node%simple_cmd%num_words
588 expanded_line = trim(expanded_line) // ' ' // &
589 trim(node%simple_cmd%words(word_i))
590 end do
591 alias_ast => parse_command_line(trim(expanded_line))
592 if (associated(alias_ast)) then
593 shell%bypass_aliases = .true.
594 exit_status = execute_ast_node(alias_ast, shell)
595 shell%bypass_aliases = .false.
596 call destroy_command_node(alias_ast)
597 end if
598 end block
599 return
600 end if
601 end do
602 end block
603 end if
604
605 ! Check if this is a function call (unless bypass_functions is set)
606 if (node%simple_cmd%num_words >= 1) then
607 cmd_name = trim(node%simple_cmd%words(1))
608 else
609 cmd_name = ''
610 end if
611
612 if (.not. shell%bypass_functions .and. len_trim(cmd_name) > 0) then
613 do func_idx = 1, num_cached_functions
614 if (trim(function_ast_cache(func_idx)%name) == trim(cmd_name)) then
615 ! This is a function call! Execute the cached AST body
616
617 ! Save old positional parameters
618 old_num_positional = shell%num_positional
619 if (allocated(shell%positional_params) .and. old_num_positional > 0) then
620 allocate(old_params(old_num_positional))
621 block
622 integer :: k
623 do k = 1, old_num_positional
624 old_params(k)%str = shell%positional_params(k)%str
625 end do
626 end block
627 end if
628
629 ! Set new positional parameters from function arguments
630 ! Need to expand arguments (variable expansion, command substitution, etc.)
631 if (node%simple_cmd%num_words > 1) then
632 ! Create temporary command to expand arguments
633 block
634 use pipeline_helpers, only: expand_tokens
635 type(command_t) :: temp_cmd
636 integer :: k
637
638 temp_cmd%num_tokens = node%simple_cmd%num_words - 1 ! Exclude function name
639 allocate(character(len=MAX_TOKEN_LEN) :: temp_cmd%tokens(temp_cmd%num_tokens))
640 allocate(temp_cmd%token_quoted(temp_cmd%num_tokens))
641 allocate(temp_cmd%token_escaped(temp_cmd%num_tokens))
642 allocate(temp_cmd%token_quote_type(temp_cmd%num_tokens))
643 allocate(temp_cmd%token_lengths(temp_cmd%num_tokens))
644
645 ! Copy arguments (skip function name at index 1)
646 do k = 1, temp_cmd%num_tokens
647 ! For quoted words, use word_lengths to extract exact content (preserves trailing whitespace)
648 if (allocated(node%simple_cmd%word_was_quoted) .and. k+1 <= size(node%simple_cmd%word_was_quoted) .and. &
649 node%simple_cmd%word_was_quoted(k + 1) .and. &
650 allocated(node%simple_cmd%word_lengths) .and. k+1 <= size(node%simple_cmd%word_lengths)) then
651 ! Quoted word - use actual length to preserve trailing whitespace
652 temp_cmd%tokens(k) = node%simple_cmd%words(k + 1)(1:node%simple_cmd%word_lengths(k + 1))
653 temp_cmd%token_lengths(k) = node%simple_cmd%word_lengths(k + 1)
654 else
655 ! Unquoted word - trim is safe
656 temp_cmd%tokens(k) = trim(node%simple_cmd%words(k + 1))
657 temp_cmd%token_lengths(k) = len_trim(node%simple_cmd%words(k + 1))
658 end if
659 if (allocated(node%simple_cmd%word_was_quoted) .and. k+1 <= size(node%simple_cmd%word_was_quoted)) then
660 temp_cmd%token_quoted(k) = node%simple_cmd%word_was_quoted(k + 1)
661 else
662 temp_cmd%token_quoted(k) = .false.
663 end if
664 if (allocated(node%simple_cmd%word_was_escaped) .and. k+1 <= size(node%simple_cmd%word_was_escaped)) then
665 temp_cmd%token_escaped(k) = node%simple_cmd%word_was_escaped(k + 1)
666 else
667 temp_cmd%token_escaped(k) = .false.
668 end if
669 if (allocated(node%simple_cmd%word_quote_type) .and. k+1 <= size(node%simple_cmd%word_quote_type)) then
670 temp_cmd%token_quote_type(k) = node%simple_cmd%word_quote_type(k + 1)
671 else
672 temp_cmd%token_quote_type(k) = QUOTE_NONE
673 end if
674 end do
675
676 ! Expand the tokens (command substitution, arithmetic, variables, etc.)
677 call expand_tokens(temp_cmd, shell)
678
679 ! Now use expanded tokens as positional parameters
680 shell%num_positional = temp_cmd%num_tokens
681 if (shell%num_positional > 0) then
682 if (.not. allocated(shell%positional_params)) then
683 allocate(shell%positional_params(shell%num_positional))
684 shell%positional_params_capacity = shell%num_positional
685 else if (shell%positional_params_capacity < shell%num_positional) then
686 deallocate(shell%positional_params)
687 allocate(shell%positional_params(shell%num_positional))
688 shell%positional_params_capacity = shell%num_positional
689 end if
690 do k = 1, shell%num_positional
691 shell%positional_params(k)%str = trim(temp_cmd%tokens(k))
692 end do
693 end if
694
695 ! Cleanup
696 if (allocated(temp_cmd%tokens)) deallocate(temp_cmd%tokens)
697 if (allocated(temp_cmd%token_quoted)) deallocate(temp_cmd%token_quoted)
698 if (allocated(temp_cmd%token_escaped)) deallocate(temp_cmd%token_escaped)
699 if (allocated(temp_cmd%token_quote_type)) deallocate(temp_cmd%token_quote_type)
700 if (allocated(temp_cmd%token_lengths)) deallocate(temp_cmd%token_lengths)
701 end block
702 else
703 shell%num_positional = 0
704 end if
705
706 ! Apply redirections for the function call
707 block
708 use fd_redirection, only: apply_single_redirection
709 use parser, only: expand_variables
710 type(redirection_t) :: temp_redirect
711 logical :: redir_success, func_has_redirects
712 character(len=:), allocatable :: expanded_filename
713 integer :: redir_idx, func_fd_mark
714
715 func_has_redirects = (node%simple_cmd%num_redirects > 0)
716 func_fd_mark = save_fd_mark()
717 if (func_has_redirects) then
718 do redir_idx = 1, node%simple_cmd%num_redirects
719 temp_redirect%type = node%simple_cmd%redirects(redir_idx)%type
720 temp_redirect%fd = node%simple_cmd%redirects(redir_idx)%fd
721 temp_redirect%target_fd = node%simple_cmd%redirects(redir_idx)%target_fd
722 if (allocated(node%simple_cmd%redirects(redir_idx)%filename)) then
723 call expand_variables(trim(node%simple_cmd%redirects(redir_idx)%filename), expanded_filename, shell)
724 if (allocated(expanded_filename)) then
725 allocate(temp_redirect%filename, source=trim(expanded_filename))
726 deallocate(expanded_filename)
727 else
728 allocate(temp_redirect%filename, source=trim(node%simple_cmd%redirects(redir_idx)%filename))
729 end if
730 end if
731 temp_redirect%force_clobber = node%simple_cmd%redirects(redir_idx)%force_clobber
732
733 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
734 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
735 if (.not. redir_success) then
736 call restore_fds_to_mark(func_fd_mark)
737 exit_status = 1
738 ! Restore old positional params before returning
739 shell%num_positional = old_num_positional
740 if (allocated(old_params)) then
741 if (shell%num_positional > 0) then
742 block
743 integer :: k
744 do k = 1, old_num_positional
745 shell%positional_params(k)%str = old_params(k)%str
746 end do
747 end block
748 end if
749 deallocate(old_params)
750 end if
751 return
752 end if
753 end do
754 end if
755
756 ! Increment function depth for return/exit context tracking
757 shell%function_depth = shell%function_depth + 1
758
759 ! Execute function body
760 ! Save body pointer locally so unset -f during execution can't
761 ! invalidate the pointer through the cache (Fortran aliasing)
762 block
763 type(command_node_t), pointer :: func_body
764 func_body => function_ast_cache(func_idx)%body
765 if (associated(func_body)) then
766 exit_status = execute_ast_node(func_body, shell)
767 else
768 exit_status = 0
769 end if
770 end block
771
772 ! Fire RETURN trap if set (before cleanup, while still in function scope)
773 block
774 use signal_handling, only: get_trap_command, TRAP_RETURN
775 character(len=256) :: return_trap_cmd
776 return_trap_cmd = get_trap_command(shell, TRAP_RETURN)
777 if (len_trim(return_trap_cmd) > 0 .and. &
778 .not. shell%executing_trap) then
779 block
780 use grammar_parser, only: parse_command_line
781 use command_tree, only: destroy_command_node
782 type(command_node_t), pointer :: trap_node
783 integer :: saved_status_rt
784 logical :: saved_bypass_rt
785 saved_status_rt = shell%last_exit_status
786 saved_bypass_rt = shell%bypass_functions
787 shell%bypass_functions = .false.
788 shell%executing_trap = .true.
789 trap_node => parse_command_line(trim(return_trap_cmd))
790 if (associated(trap_node)) then
791 exit_status = execute_ast_node(trap_node, shell)
792 call destroy_command_node(trap_node)
793 end if
794 shell%executing_trap = .false.
795 shell%bypass_functions = saved_bypass_rt
796 shell%last_exit_status = saved_status_rt
797 end block
798 end if
799 end block
800
801 ! Clean up local variables for this function scope
802 if (shell%function_depth > 0 .and. &
803 shell%function_depth <= size(shell%local_var_counts)) then
804 ! Check if IFS was local — restore from global variables
805 block
806 integer :: lv_idx, gv_idx
807 logical :: had_local_ifs, found_global_ifs
808 had_local_ifs = .false.
809 do lv_idx = 1, shell%local_var_counts(shell%function_depth)
810 if (trim(shell%local_vars(shell%function_depth, lv_idx)%name) == 'IFS') then
811 had_local_ifs = .true.
812 end if
813 ! Clean up local arrays from global variable storage
814 if (shell%local_vars(shell%function_depth, lv_idx)%is_array) then
815 do gv_idx = 1, shell%num_variables
816 if (trim(shell%variables(gv_idx)%name) == &
817 trim(shell%local_vars(shell%function_depth, lv_idx)%name)) then
818 shell%variables(gv_idx)%name = ''
819 shell%variables(gv_idx)%value = ''
820 shell%variables(gv_idx)%is_array = .false.
821 shell%variables(gv_idx)%array_size = 0
822 exit
823 end if
824 end do
825 end if
826 end do
827 shell%local_var_counts(shell%function_depth) = 0
828 if (had_local_ifs) then
829 ! Look up IFS from global variables array, bypassing local scope
830 found_global_ifs = .false.
831 do gv_idx = 1, shell%num_variables
832 if (trim(shell%variables(gv_idx)%name) == 'IFS') then
833 shell%ifs_len = shell%variables(gv_idx)%value_len
834 if (shell%ifs_len <= 0) &
835 shell%ifs_len = len_trim(shell%variables(gv_idx)%value)
836 if (shell%ifs_len > len(shell%ifs)) &
837 shell%ifs_len = len(shell%ifs)
838 shell%ifs = ''
839 if (shell%ifs_len > 0) shell%ifs(1:shell%ifs_len) = &
840 shell%variables(gv_idx)%value(1:shell%ifs_len)
841 found_global_ifs = .true.
842 exit
843 end if
844 end do
845 if (.not. found_global_ifs) then
846 ! IFS was never explicitly set globally — restore default
847 shell%ifs = ' ' // char(9) // char(10)
848 shell%ifs_len = 3
849 end if
850 end if
851 end block
852 end if
853
854 ! Decrement function depth
855 shell%function_depth = shell%function_depth - 1
856
857 ! Restore file descriptors if we applied redirections
858 if (func_has_redirects) then
859 call restore_fds_to_mark(func_fd_mark)
860 end if
861 end block
862
863 ! Clear function return flag and use return value as exit status
864 if (shell%function_return_pending) then
865 exit_status = shell%function_return_value
866 shell%function_return_pending = .false.
867 end if
868
869 ! Restore old positional parameters
870 shell%num_positional = old_num_positional
871 if (allocated(old_params)) then
872 if (shell%num_positional > 0) then
873 block
874 integer :: k
875 do k = 1, old_num_positional
876 shell%positional_params(k)%str = old_params(k)%str
877 end do
878 end block
879 end if
880 deallocate(old_params)
881 end if
882
883 ! POSIX: exit in function should exit shell, not just return from function
884 ! Preserve shell%last_exit_status which was set by builtin_exit
885 if (.not. shell%running) then
886 exit_status = shell%last_exit_status
887 end if
888
889 return
890 end if
891 end do
892 end if ! .not. shell%bypass_functions
893
894 ! Convert AST simple command to command_t format for legacy executor dispatch.
895 ! Pipeline execution is now handled directly by execute_pipeline_node (Phases 1-6).
896 ! This conversion remains for individual command execution (builtins, externals,
897 ! assignments, aliases, etc.) which still delegates to execute_single.
898
899 allocate(temp_pipeline%commands(1))
900 temp_pipeline%num_commands = 1
901
902 ! Initialize command in place (avoid structure copy issues)
903 temp_pipeline%commands(1)%num_tokens = node%simple_cmd%num_words
904 temp_pipeline%commands(1)%separator = SEP_NONE
905 temp_pipeline%commands(1)%background = .false.
906 temp_pipeline%commands(1)%num_redirections = 0
907 ! Copy prefix assignments from AST
908 temp_pipeline%commands(1)%num_prefix_assignments = node%simple_cmd%num_assignments
909 if (node%simple_cmd%num_assignments > 0 .and. allocated(node%simple_cmd%assignments)) then
910 if (.not. allocated(temp_pipeline%commands(1)%prefix_assignments)) then
911 allocate(character(len=MAX_TOKEN_LEN) :: &
912 temp_pipeline%commands(1)%prefix_assignments(MAX_PREFIX_ASSIGNMENTS))
913 end if
914 do i = 1, node%simple_cmd%num_assignments
915 temp_pipeline%commands(1)%prefix_assignments(i) = node%simple_cmd%assignments(i)
916 end do
917 end if
918 ! Check if words were pre-expanded in pipeline
919 temp_pipeline%commands(1)%skip_expansion = node%simple_cmd%pre_expanded
920
921
922 ! Allocate tokens array — use actual word width if pre-expanded (may be > MAX_TOKEN_LEN)
923 block
924 integer :: tok_alloc_len
925 if (node%simple_cmd%pre_expanded .and. allocated(node%simple_cmd%words) .and. &
926 node%simple_cmd%num_words > 0) then
927 tok_alloc_len = max(MAX_TOKEN_LEN, len(node%simple_cmd%words(1)))
928 else
929 tok_alloc_len = MAX_TOKEN_LEN
930 end if
931 allocate(character(len=tok_alloc_len) :: temp_pipeline%commands(1)%tokens(node%simple_cmd%num_words))
932 end block
933
934 ! Allocate metadata arrays to track token properties
935 allocate(temp_pipeline%commands(1)%token_quoted(node%simple_cmd%num_words))
936 allocate(temp_pipeline%commands(1)%token_escaped(node%simple_cmd%num_words))
937 allocate(temp_pipeline%commands(1)%token_quote_type(node%simple_cmd%num_words))
938 allocate(temp_pipeline%commands(1)%token_lengths(node%simple_cmd%num_words))
939
940 ! Initialize metadata arrays
941 temp_pipeline%commands(1)%token_quoted = .false.
942 temp_pipeline%commands(1)%token_escaped = .false.
943 temp_pipeline%commands(1)%token_quote_type = QUOTE_NONE
944 temp_pipeline%commands(1)%token_lengths = 0
945
946 ! Copy words to tokens and metadata
947 do i = 1, node%simple_cmd%num_words
948 ! For quoted words, use word_lengths to extract exact content (preserves trailing whitespace)
949 ! For unquoted words, trim is safe
950 if (allocated(node%simple_cmd%word_was_quoted) .and. &
951 i <= size(node%simple_cmd%word_was_quoted) .and. &
952 node%simple_cmd%word_was_quoted(i) .and. &
953 allocated(node%simple_cmd%word_lengths) .and. &
954 i <= size(node%simple_cmd%word_lengths)) then
955 ! Quoted word - use actual length to preserve trailing whitespace
956 temp_pipeline%commands(1)%tokens(i) = node%simple_cmd%words(i)(1:node%simple_cmd%word_lengths(i))
957 temp_pipeline%commands(1)%token_lengths(i) = node%simple_cmd%word_lengths(i)
958 else
959 ! Unquoted word - trim is safe
960 temp_pipeline%commands(1)%tokens(i) = trim(node%simple_cmd%words(i))
961 temp_pipeline%commands(1)%token_lengths(i) = len_trim(node%simple_cmd%words(i))
962 end if
963
964 ! Copy metadata if available
965 if (allocated(node%simple_cmd%word_was_quoted) .and. &
966 i <= size(node%simple_cmd%word_was_quoted)) then
967 temp_pipeline%commands(1)%token_quoted(i) = node%simple_cmd%word_was_quoted(i)
968 end if
969
970 if (allocated(node%simple_cmd%word_was_escaped) .and. &
971 i <= size(node%simple_cmd%word_was_escaped)) then
972 temp_pipeline%commands(1)%token_escaped(i) = node%simple_cmd%word_was_escaped(i)
973 end if
974
975 if (allocated(node%simple_cmd%word_quote_type) .and. &
976 i <= size(node%simple_cmd%word_quote_type)) then
977 temp_pipeline%commands(1)%token_quote_type(i) = node%simple_cmd%word_quote_type(i)
978 end if
979 end do
980
981 ! Copy heredoc delimiter if present (content will be read by executor)
982 if (len_trim(node%simple_cmd%heredoc_delimiter) > 0) then
983 temp_pipeline%commands(1)%heredoc_delimiter = trim(node%simple_cmd%heredoc_delimiter)
984 temp_pipeline%commands(1)%heredoc_quoted = node%simple_cmd%heredoc_quoted
985 temp_pipeline%commands(1)%heredoc_strip_tabs = node%simple_cmd%heredoc_strip_tabs
986 end if
987
988 ! POSIX: For pure assignments (no command), process assignments BEFORE redirections
989 ! This ensures assignment errors go to the shell's original stderr, not the redirected one
990 if (node%simple_cmd%num_words > 0) then
991 ! Check if all words are assignments (VAR=value pattern)
992 is_pure_assignment = .true.
993 do i = 1, node%simple_cmd%num_words
994 eq_pos = index(trim(node%simple_cmd%words(i)), '=')
995 if (eq_pos <= 1) then
996 is_pure_assignment = .false.
997 exit
998 end if
999 ! Check that everything before = is a valid var name
1000 if (.not. is_valid_assignment_name(node%simple_cmd%words(i)(1:eq_pos-1))) then
1001 is_pure_assignment = .false.
1002 exit
1003 end if
1004 end do
1005
1006 if (is_pure_assignment) then
1007 ! Execute assignments before redirections
1008 call execute_pipeline(temp_pipeline, shell, trim(shell%current_command))
1009 exit_status = shell%last_exit_status
1010
1011 ! Clean up and return - skip redirections for pure assignments
1012 if (allocated(temp_pipeline%commands)) then
1013 if (allocated(temp_pipeline%commands(1)%tokens)) deallocate(temp_pipeline%commands(1)%tokens)
1014 if (allocated(temp_pipeline%commands(1)%token_quoted)) deallocate(temp_pipeline%commands(1)%token_quoted)
1015 if (allocated(temp_pipeline%commands(1)%token_escaped)) deallocate(temp_pipeline%commands(1)%token_escaped)
1016 if (allocated(temp_pipeline%commands(1)%token_quote_type)) deallocate(temp_pipeline%commands(1)%token_quote_type)
1017 if (allocated(temp_pipeline%commands(1)%token_lengths)) deallocate(temp_pipeline%commands(1)%token_lengths)
1018 deallocate(temp_pipeline%commands)
1019 end if
1020 return
1021 end if
1022 end if
1023
1024 ! Apply redirections directly (in order, left-to-right) before executing
1025 ! This preserves proper ordering for cases like: echo test >/tmp/r1 2>&1 >/tmp/r2
1026 has_redirects = (node%simple_cmd%num_redirects > 0)
1027 fd_mark = save_fd_mark()
1028 if (has_redirects) then
1029 do i = 1, node%simple_cmd%num_redirects
1030 temp_redirect%type = node%simple_cmd%redirects(i)%type
1031 temp_redirect%fd = node%simple_cmd%redirects(i)%fd
1032 temp_redirect%target_fd = node%simple_cmd%redirects(i)%target_fd
1033 if (allocated(node%simple_cmd%redirects(i)%filename)) then
1034 ! Expand variables in redirect filename (e.g., /tmp/file$$)
1035 ! For here-strings, preserve trailing whitespace
1036 if (temp_redirect%type == REDIR_HERE_STRING) then
1037 block
1038 integer :: hs_len
1039 hs_len = len(node%simple_cmd%redirects(i)%filename)
1040 if (index(node%simple_cmd%redirects(i)%filename, &
1041 '$') > 0 .or. &
1042 index(node%simple_cmd%redirects(i)%filename, &
1043 '`') > 0) then
1044 call expand_variables( &
1045 node%simple_cmd%redirects(i)%filename, &
1046 expanded_filename, shell)
1047 if (allocated(expanded_filename)) then
1048 allocate(temp_redirect%filename, &
1049 source=expanded_filename)
1050 else
1051 allocate(temp_redirect%filename, &
1052 source= &
1053 node%simple_cmd%redirects(i)%filename)
1054 end if
1055 else
1056 allocate(temp_redirect%filename, &
1057 source= &
1058 node%simple_cmd%redirects(i)%filename)
1059 end if
1060 end block
1061 else
1062 call expand_variables( &
1063 trim(node%simple_cmd%redirects(i)%filename), &
1064 expanded_filename, shell)
1065 if (allocated(expanded_filename)) then
1066 allocate(temp_redirect%filename, &
1067 source=expanded_filename)
1068 else
1069 allocate(temp_redirect%filename, &
1070 source=trim( &
1071 node%simple_cmd%redirects(i)%filename))
1072 end if
1073 end if
1074 end if
1075 temp_redirect%force_clobber = node%simple_cmd%redirects(i)%force_clobber
1076
1077 ! For dup redirections with filename (variable-expanded fd),
1078 ! parse the expanded filename as fd number
1079 if ((temp_redirect%type == REDIR_DUP_OUT .or. &
1080 temp_redirect%type == REDIR_DUP_IN) .and. &
1081 allocated(temp_redirect%filename)) then
1082 block
1083 integer :: dup_fd, dup_ios
1084 read(temp_redirect%filename, *, &
1085 iostat=dup_ios) dup_fd
1086 if (dup_ios == 0) then
1087 temp_redirect%target_fd = dup_fd
1088 deallocate(temp_redirect%filename)
1089 end if
1090 end block
1091 end if
1092
1093 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
1094 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
1095 if (.not. redir_success) then
1096 exit_status = 1
1097 if (allocated(temp_pipeline%commands)) then
1098 if (allocated(temp_pipeline%commands(1)%tokens)) deallocate(temp_pipeline%commands(1)%tokens)
1099 if (allocated(temp_pipeline%commands(1)%token_quoted)) deallocate(temp_pipeline%commands(1)%token_quoted)
1100 if (allocated(temp_pipeline%commands(1)%token_escaped)) deallocate(temp_pipeline%commands(1)%token_escaped)
1101 if (allocated(temp_pipeline%commands(1)%token_quote_type)) deallocate(temp_pipeline%commands(1)%token_quote_type)
1102 if (allocated(temp_pipeline%commands(1)%token_lengths)) deallocate(temp_pipeline%commands(1)%token_lengths)
1103 deallocate(temp_pipeline%commands)
1104 end if
1105 call restore_fds_to_mark(fd_mark)
1106 return
1107 end if
1108 end do
1109 end if
1110
1111 ! Execute using existing executor
1112 ! Note: Pass empty command line - tokens array is what matters
1113 call execute_pipeline(temp_pipeline, shell, trim(shell%current_command))
1114
1115 exit_status = shell%last_exit_status
1116 ! POSIX: Check errexit after command execution (including assignments with command substitutions)
1117 call check_errexit(shell, exit_status)
1118 ! If errexit triggered, return immediately
1119 if (.not. shell%running) then
1120 return
1121 end if
1122
1123 ! Check if fatal expansion error occurred (e.g., set -u with undefined variable)
1124 if (shell%fatal_expansion_error) then
1125 ! NOTE: Don't reset fatal_expansion_error here - let it propagate to subshell handler
1126 ! The subshell code needs to know about the error to adjust exit code (127 -> 1)
1127 ! POSIX: In non-interactive shells, exit the shell entirely
1128 if (.not. shell%is_interactive) then
1129 shell%running = .false.
1130 end if
1131 ! Exit status was already set by expansion code (usually 127)
1132 ! Just clean up and return
1133 if (has_redirects) then
1134 call restore_fds_to_mark(fd_mark)
1135 end if
1136 if (allocated(temp_pipeline%commands)) then
1137 if (allocated(temp_pipeline%commands(1)%tokens)) deallocate(temp_pipeline%commands(1)%tokens)
1138 if (allocated(temp_pipeline%commands(1)%token_quoted)) deallocate(temp_pipeline%commands(1)%token_quoted)
1139 if (allocated(temp_pipeline%commands(1)%token_escaped)) deallocate(temp_pipeline%commands(1)%token_escaped)
1140 if (allocated(temp_pipeline%commands(1)%token_quote_type)) deallocate(temp_pipeline%commands(1)%token_quote_type)
1141 if (allocated(temp_pipeline%commands(1)%token_lengths)) deallocate(temp_pipeline%commands(1)%token_lengths)
1142 deallocate(temp_pipeline%commands)
1143 end if
1144 return
1145 end if
1146
1147 ! Restore file descriptors if we applied any redirections
1148 if (has_redirects) then
1149 call restore_fds_to_mark(fd_mark)
1150 end if
1151
1152 ! Check for pending signals and dispatch their trap handlers
1153 if (.not. shell%executing_trap) then
1154 call dispatch_pending_signals(shell)
1155 end if
1156
1157 ! If a trap command was queued, execute it now (unless we're already executing a trap)
1158 if (len_trim(shell%pending_trap_command) > 0 .and. .not. shell%executing_trap) then
1159 call execute_pending_trap(shell)
1160 end if
1161
1162 ! POSIX: Update $_ to last argument of previous command
1163 if (node%simple_cmd%num_words > 0 .and. allocated(node%simple_cmd%words)) then
1164 shell%last_arg = trim(node%simple_cmd%words(node%simple_cmd%num_words))
1165 end if
1166
1167 ! Clean up
1168 if (allocated(temp_pipeline%commands)) then
1169 if (allocated(temp_pipeline%commands(1)%tokens)) deallocate(temp_pipeline%commands(1)%tokens)
1170 if (allocated(temp_pipeline%commands(1)%token_quoted)) deallocate(temp_pipeline%commands(1)%token_quoted)
1171 if (allocated(temp_pipeline%commands(1)%token_escaped)) deallocate(temp_pipeline%commands(1)%token_escaped)
1172 if (allocated(temp_pipeline%commands(1)%token_quote_type)) deallocate(temp_pipeline%commands(1)%token_quote_type)
1173 if (allocated(temp_pipeline%commands(1)%token_lengths)) deallocate(temp_pipeline%commands(1)%token_lengths)
1174 deallocate(temp_pipeline%commands)
1175 end if
1176
1177 end function execute_simple_command
1178
1179 ! =====================================
1180 ! Pipeline Execution
1181 ! =====================================
1182
1183 function execute_pipeline_node(node, shell) result(exit_status)
1184 type(command_node_t), pointer, intent(in) :: node
1185 type(shell_state_t), intent(inout) :: shell
1186 integer :: exit_status
1187 integer :: i, status, ret, pipe_idx
1188 integer(c_int), allocatable, target :: pipefd(:,:)
1189 integer(c_pid_t), allocatable :: pids(:)
1190 integer(c_pid_t) :: pgid
1191 integer :: num_pipes, num_commands
1192
1193 exit_status = 0
1194
1195 if (.not. associated(node%pipeline)) then
1196 exit_status = 0
1197 return
1198 end if
1199
1200 if (node%pipeline%num_commands == 0) then
1201 exit_status = 0
1202 return
1203 end if
1204
1205 if (node%pipeline%num_commands == 1) then
1206 ! Single command - no piping needed
1207 if (associated(node%pipeline%commands)) then
1208 ! POSIX: Suppress errexit for negated pipelines
1209 if (node%pipeline%negate) shell%in_negation = .true.
1210 exit_status = execute_ast_node(node%pipeline%commands(1), shell)
1211 shell%in_negation = .false.
1212 end if
1213
1214 ! Handle negation
1215 if (node%pipeline%negate) then
1216 if (exit_status == 0) then
1217 exit_status = 1
1218 else
1219 exit_status = 0
1220 end if
1221 end if
1222 return
1223 end if
1224
1225 ! Multiple commands - set up pipes
1226 num_commands = node%pipeline%num_commands
1227 num_pipes = num_commands - 1
1228
1229 allocate(pipefd(2, num_pipes), stat=i)
1230 if (i /= 0) then
1231 write(error_unit, '(A)') 'fortsh: failed to allocate pipe descriptors'
1232 exit_status = 1
1233 return
1234 end if
1235 allocate(pids(num_commands), stat=i)
1236 if (i /= 0) then
1237 write(error_unit, '(A)') 'fortsh: failed to allocate pid array'
1238 deallocate(pipefd)
1239 exit_status = 1
1240 return
1241 end if
1242
1243 ! POSIX: Pre-expand all command words before forking
1244 ! This ensures expansion errors go to the parent shell's stderr
1245 block
1246 logical :: was_running
1247 was_running = shell%running
1248 call pre_expand_pipeline(node, shell)
1249 ! Restore shell state - pipeline should still run even if expansion failed
1250 ! The error message already went to stderr; pipeline continues with expanded values
1251 shell%fatal_expansion_error = .false.
1252 shell%arithmetic_error = .false.
1253 shell%running = was_running
1254 end block
1255
1256 ! Create all pipes
1257 do i = 1, num_pipes
1258 if (c_pipe(c_loc(pipefd(1, i))) /= 0) then
1259 write(error_unit, '(A)') 'fortsh: pipe creation failed'
1260 exit_status = 1
1261 deallocate(pipefd)
1262 deallocate(pids)
1263 return
1264 end if
1265 end do
1266
1267 ! Xtrace: trace all pipeline commands BEFORE forking (deterministic order)
1268 ! Child processes will have xtrace suppressed to avoid double-tracing
1269 if (shell%option_xtrace) then
1270 call ast_trace_pipeline(node, shell)
1271 end if
1272
1273 ! Flush all output before forking to prevent buffer duplication
1274 flush(output_unit)
1275 flush(error_unit)
1276
1277 ! Fork and execute each command in the pipeline
1278 do i = 1, num_commands
1279 pids(i) = c_fork()
1280
1281 if (pids(i) == 0) then
1282 ! Child process — mark as pipeline child so execute_external
1283 ! skips setpgid/tcsetpgrp (managed at pipeline level instead)
1284 shell%in_pipeline_child = .true.
1285
1286 ! Set process group: all pipeline children share the first child's PID
1287 ! as their process group. Both child and parent call setpgid (race-free).
1288 if (i == 1) then
1289 pgid = c_getpid()
1290 else
1291 pgid = pids(1)
1292 end if
1293 ret = c_setpgid(0, pgid)
1294
1295 ! Reset all signal handlers to default for pipeline children.
1296 ! Safe now because execute_external skips its own setpgid/tcsetpgrp
1297 ! when in_pipeline_child is set, so SIGTTOU won't stop the process.
1298 block
1299 type(c_funptr) :: old_handler
1300 old_handler = c_signal(SIGINT, c_null_funptr)
1301 old_handler = c_signal(SIGPIPE, c_null_funptr)
1302 old_handler = c_signal(SIGTSTP, c_null_funptr)
1303 old_handler = c_signal(SIGTTIN, c_null_funptr)
1304 old_handler = c_signal(SIGTTOU, c_null_funptr)
1305 end block
1306
1307 ! Set up stdin from previous pipe
1308 if (i > 1) then
1309 pipe_idx = i - 1
1310 ret = c_dup2(pipefd(1, pipe_idx), int(0, c_int)) ! Read from previous pipe
1311 end if
1312
1313 ! Set up stdout to next pipe
1314 if (i < num_commands) then
1315 ret = c_dup2(pipefd(2, i), int(1, c_int)) ! Write to next pipe
1316 end if
1317
1318 ! Close all pipe fds
1319 call close_all_pipes(pipefd, num_pipes)
1320
1321 ! Suppress xtrace in child — parent already traced deterministically
1322 shell%option_xtrace = .false.
1323
1324 ! POSIX: Only ignored traps (empty action) are visible in subshells
1325 ! Remove traps with commands, but keep traps with empty actions (ignore)
1326 call filter_traps_for_subshell(shell)
1327
1328 ! Execute command
1329 status = execute_ast_node(node%pipeline%commands(i), shell)
1330 call c_exit(status)
1331 end if
1332
1333 ! Parent: set process group (race-free — both parent and child call setpgid)
1334 if (pids(i) > 0) then
1335 if (i == 1) then
1336 pgid = pids(1)
1337 end if
1338 ret = c_setpgid(pids(i), pgid)
1339 else
1340 ! Fork failed — close all pipes and return
1341 write(error_unit, '(A)') 'fortsh: fork: resource temporarily unavailable'
1342 call close_all_pipes(pipefd, num_pipes)
1343 exit_status = 1
1344 return
1345 end if
1346 end do
1347
1348 ! Parent process - close all pipes
1349 call close_all_pipes(pipefd, num_pipes)
1350
1351 if (node%pipeline%background) then
1352 ! Background pipeline: add job, don't wait
1353 block
1354 integer :: job_id
1355 character(len=:), allocatable :: job_command
1356 job_command = ''
1357 ! Reconstruct command string from pipeline words
1358 do i = 1, node%pipeline%num_commands
1359 if (i > 1) job_command = trim(job_command) // ' | '
1360 if (associated(node%pipeline%commands(i)%simple_cmd)) then
1361 block
1362 integer :: w
1363 do w = 1, node%pipeline%commands(i)%simple_cmd%num_words
1364 if (w == 1 .and. i == 1) then
1365 job_command = trim(node%pipeline%commands(i)%simple_cmd%words(w))
1366 else if (w == 1) then
1367 job_command = trim(job_command) // &
1368 trim(node%pipeline%commands(i)%simple_cmd%words(w))
1369 else
1370 job_command = trim(job_command) // ' ' // &
1371 trim(node%pipeline%commands(i)%simple_cmd%words(w))
1372 end if
1373 end do
1374 end block
1375 end if
1376 end do
1377 job_id = add_job(shell, pgid, trim(job_command), .false.)
1378 if (shell%is_interactive) then
1379 write(output_unit, '(a,i0,a,i0)') '[', job_id, '] ', pids(1)
1380 end if
1381 shell%last_bg_pid = pids(num_commands)
1382 end block
1383 exit_status = 0
1384 else
1385 ! Foreground pipeline: give terminal, wait, restore terminal
1386 if (shell%is_interactive) then
1387 ret = c_tcsetpgrp(shell%shell_terminal, pgid)
1388 end if
1389
1390 ! Wait for all children and collect exit statuses
1391 block
1392 integer(c_int), target :: wait_status
1393 integer, allocatable :: exit_statuses(:)
1394 logical :: pipeline_stopped
1395 integer :: stopped_job_id
1396 character(len=1024) :: stopped_cmd
1397 allocate(exit_statuses(num_commands))
1398 pipeline_stopped = .false.
1399
1400 do i = 1, num_commands
1401 ret = c_waitpid(pids(i), c_loc(wait_status), WUNTRACED)
1402 if (ret > 0) then
1403 if (WIFEXITED(wait_status)) then
1404 exit_statuses(i) = WEXITSTATUS(wait_status)
1405 else if (WIFSIGNALED(wait_status)) then
1406 exit_statuses(i) = 128 + WTERMSIG(wait_status)
1407 else if (WIFSTOPPED(wait_status)) then
1408 ! Process was stopped (Ctrl+Z) — add to job table as stopped
1409 exit_statuses(i) = 128 + WSTOPSIG(wait_status)
1410 ! Pipeline process was stopped (Ctrl+Z)
1411 ! Note: simple commands go through the legacy executor which
1412 ! handles WIFSTOPPED, add_job, and Stopped message. This path
1413 ! only runs for multi-command pipelines.
1414 if (.not. pipeline_stopped) then
1415 pipeline_stopped = .true.
1416 stopped_cmd = trim(shell%current_command)
1417 if (len_trim(stopped_cmd) == 0) stopped_cmd = '<stopped job>'
1418 stopped_job_id = add_job(shell, pgid, trim(stopped_cmd), .true.)
1419 ! Mark job as stopped in the job table
1420 block
1421 integer :: ji
1422 do ji = 1, MAX_JOBS
1423 if (shell%jobs(ji)%job_id == stopped_job_id) then
1424 shell%jobs(ji)%state = JOB_STOPPED
1425 exit
1426 end if
1427 end do
1428 end block
1429 write(output_unit, '()')
1430 write(output_unit, '(a)') 'Stopped'
1431 flush(output_unit)
1432 end if
1433 else
1434 exit_statuses(i) = 1
1435 end if
1436 else
1437 exit_statuses(i) = 1
1438 end if
1439 end do
1440
1441 ! Populate PIPESTATUS array (bash extension)
1442 block
1443 use variables, only: set_array_variable
1444 character(len=16) :: pipestatus_vals(num_commands)
1445 do i = 1, num_commands
1446 write(pipestatus_vals(i), '(I0)') exit_statuses(i)
1447 end do
1448 call set_array_variable(shell, 'PIPESTATUS', pipestatus_vals, num_commands)
1449 end block
1450
1451 ! POSIX default: exit status from last command
1452 ! pipefail: rightmost non-zero exit status
1453 if (shell%option_pipefail) then
1454 exit_status = 0
1455 do i = num_commands, 1, -1
1456 if (exit_statuses(i) /= 0) then
1457 exit_status = exit_statuses(i)
1458 exit
1459 end if
1460 end do
1461 else
1462 exit_status = exit_statuses(num_commands)
1463 end if
1464
1465 deallocate(exit_statuses)
1466 end block
1467
1468 ! Restore terminal control to the shell's process group
1469 if (shell%is_interactive) then
1470 ret = c_tcsetpgrp(shell%shell_terminal, shell%shell_pgid)
1471 end if
1472 end if
1473
1474 deallocate(pipefd)
1475 deallocate(pids)
1476
1477 ! Handle negation
1478 if (node%pipeline%negate) then
1479 if (exit_status == 0) then
1480 exit_status = 1
1481 else
1482 exit_status = 0
1483 end if
1484 end if
1485
1486 ! POSIX: Check errexit after pipeline execution (e.g., pipefail + errexit)
1487 shell%last_exit_status = exit_status
1488 call check_errexit(shell, exit_status)
1489
1490 end function execute_pipeline_node
1491
1492 ! =====================================
1493 ! List Execution (;, &&, ||, &)
1494 ! =====================================
1495
1496 recursive function execute_list_node(node, shell) result(exit_status)
1497 use iso_fortran_env, only: error_unit
1498 type(command_node_t), pointer, intent(in) :: node
1499 type(shell_state_t), intent(inout) :: shell
1500 integer :: exit_status, left_status
1501 integer(c_pid_t) :: pid
1502 integer :: status
1503 character(len=:), allocatable :: job_command, job_command2
1504 integer :: i, j
1505
1506 exit_status = 0
1507
1508 if (.not. associated(node%list)) then
1509 return
1510 end if
1511
1512 ! For background jobs (&), handle specially - don't execute left side yet
1513 if (node%list%separator == LIST_SEP_BACKGROUND) then
1514 ! Special handling for left-associative parsing with semicolons
1515 ! For "a; b & c" which parses as "(a; b) & c", we need to:
1516 ! 1. Execute "a" in parent (synchronously)
1517 ! 2. Fork for "b" (background)
1518 ! 3. Execute "c" in parent
1519 if (associated(node%list%left)) then
1520 if (node%list%left%node_type == CMD_LIST) then
1521 if (associated(node%list%left%list)) then
1522 if (node%list%left%list%separator == LIST_SEP_SEQUENTIAL) then
1523 ! Execute the sequential commands before &, keeping the rightmost one for background
1524 ! Execute left part synchronously in parent
1525 if (associated(node%list%left%list%left)) then
1526 left_status = execute_ast_node(node%list%left%list%left, shell)
1527 end if
1528 ! Now fork only for the right part (the command immediately before &)
1529 if (associated(node%list%left%list%right)) then
1530 pid = c_fork()
1531 if (pid == 0) then
1532 shell%is_interactive = .false.
1533 shell%in_background = .true.
1534 status = execute_ast_node(node%list%left%list%right, shell)
1535 call c_exit(status)
1536 else if (pid > 0) then
1537 shell%last_bg_pid = pid
1538 ! Track job inline (duplicated code to avoid goto)
1539 if (.not. shell%in_background) then
1540 job_command = trim(shell%current_command)
1541 if (len_trim(job_command) == 0 .and. associated(node%list%left%list%right)) then
1542 if (node%list%left%list%right%node_type == CMD_SIMPLE) then
1543 if (associated(node%list%left%list%right%simple_cmd)) then
1544 if (node%list%left%list%right%simple_cmd%num_words > 0) then
1545 job_command = ''
1546 do i = 1, node%list%left%list%right%simple_cmd%num_words
1547 if (i > 1) then
1548 job_command = trim(job_command) // ' ' // trim(node%list%left%list%right%simple_cmd%words(i))
1549 else
1550 job_command = trim(node%list%left%list%right%simple_cmd%words(i))
1551 end if
1552 end do
1553 end if
1554 end if
1555 end if
1556 end if
1557 status = add_job(shell, pid, trim(job_command), .false.)
1558 if (shell%is_interactive) then
1559 write(output_unit, '(a,i0,a,i0)') '[', status, '] ', pid
1560 end if
1561 end if
1562 end if
1563 end if
1564 ! Continue with right side
1565 if (associated(node%list%right)) then
1566 exit_status = execute_ast_node(node%list%right, shell)
1567 else
1568 exit_status = 0
1569 end if
1570 return
1571 end if
1572 end if
1573 end if
1574 end if
1575
1576 ! Standard background handling (for non-sequential left sides)
1577 pid = c_fork()
1578 if (pid == 0) then
1579 ! Child process - execute left command and exit with its status
1580 ! Background jobs should not do terminal control or track sub-jobs
1581 shell%is_interactive = .false.
1582 shell%in_background = .true.
1583 ! Special case: if left side is itself a background list, execute only its left child
1584 ! This handles left-associative parsing: (a & b) & c should run a, not (a & b)
1585 if (associated(node%list%left)) then
1586 if (node%list%left%node_type == CMD_LIST) then
1587 if (associated(node%list%left%list)) then
1588 if (node%list%left%list%separator == LIST_SEP_BACKGROUND) then
1589 if (associated(node%list%left%list%left)) then
1590 status = execute_ast_node(node%list%left%list%left, shell)
1591 else
1592 status = 0
1593 end if
1594 else
1595 status = execute_ast_node(node%list%left, shell)
1596 end if
1597 else
1598 status = 0
1599 end if
1600 else
1601 status = execute_ast_node(node%list%left, shell)
1602 end if
1603 else
1604 status = 0
1605 end if
1606 call c_exit(status)
1607 else if (pid > 0) then
1608 ! Parent - add to job list and continue with right
1609 shell%last_bg_pid = pid
1610 ! Only track jobs if we're not already in a background job child
1611 if (.not. shell%in_background) then
1612 ! Use the current command text for job display
1613 job_command = trim(shell%current_command)
1614 if (len_trim(job_command) == 0) job_command = '<background job>'
1615
1616 status = add_job(shell, pid, trim(job_command), .false.)
1617 ! Only print job notification in interactive mode
1618 if (shell%is_interactive) then
1619 write(output_unit, '(a,i0,a,i0)') '[', status, '] ', pid
1620 end if
1621 end if
1622
1623 ! Special case: if left side was a nested background list, fork for its right side too
1624 ! This handles left-associative parsing: (a & b) & c should fork for both a and b
1625 if (associated(node%list%left)) then
1626 if (node%list%left%node_type == CMD_LIST) then
1627 if (associated(node%list%left%list)) then
1628 if (node%list%left%list%separator == LIST_SEP_BACKGROUND) then
1629 if (associated(node%list%left%list%right)) then
1630 pid = c_fork()
1631 if (pid == 0) then
1632 ! Child for the nested right side
1633 shell%is_interactive = .false.
1634 shell%in_background = .true.
1635 left_status = execute_ast_node(node%list%left%list%right, shell)
1636 call c_exit(left_status)
1637 else if (pid > 0) then
1638 ! Parent adds this job too
1639 shell%last_bg_pid = pid
1640 if (.not. shell%in_background) then
1641 ! Reconstruct command string for the nested right side
1642 job_command2 = '<background job>'
1643 if (associated(node%list%left%list%right)) then
1644 if (node%list%left%list%right%node_type == CMD_SIMPLE) then
1645 if (associated(node%list%left%list%right%simple_cmd)) then
1646 if (node%list%left%list%right%simple_cmd%num_words > 0) then
1647 job_command2 = ''
1648 do j = 1, node%list%left%list%right%simple_cmd%num_words
1649 if (j > 1) then
1650 job_command2 = trim(job_command2) // ' ' // trim(node%list%left%list%right%simple_cmd%words(j))
1651 else
1652 job_command2 = trim(node%list%left%list%right%simple_cmd%words(j))
1653 end if
1654 end do
1655 end if
1656 end if
1657 end if
1658 end if
1659
1660 status = add_job(shell, pid, trim(job_command2), .false.)
1661 if (shell%is_interactive) then
1662 write(output_unit, '(a,i0,a,i0)') '[', status, '] ', pid
1663 end if
1664 end if
1665 end if
1666 end if
1667 end if
1668 end if
1669 end if
1670 end if
1671
1672 if (associated(node%list%right)) then
1673 exit_status = execute_ast_node(node%list%right, shell)
1674 else
1675 exit_status = 0
1676 end if
1677 return
1678 else
1679 ! Fork failed
1680 exit_status = 1
1681 return
1682 end if
1683 end if
1684
1685 ! Execute left side (for all non-background separators)
1686 if (associated(node%list%left)) then
1687 ! POSIX: Suppress errexit during left side of AND-OR lists
1688 if (node%list%separator == LIST_SEP_AND .or. node%list%separator == LIST_SEP_OR) then
1689 shell%in_and_or_list = .true.
1690 end if
1691 left_status = execute_ast_node(node%list%left, shell)
1692 shell%in_and_or_list = .false.
1693 else
1694 left_status = 0
1695 end if
1696
1697 ! Errexit is checked at the simple command level instead
1698
1699 ! Handle based on separator type
1700 select case(node%list%separator)
1701 case(LIST_SEP_SEQUENTIAL)
1702 ! ; - Execute right side unless shell is exiting
1703 ! But first, handle any sourcing queued by the left side (e.g., dot command)
1704 if (shell%should_source) then
1705 call process_source_inline_ast(shell)
1706 end if
1707 ! Check for break/continue - if requested, skip the right side
1708 if (shell%control_depth > 0) then
1709 if (shell%control_stack(shell%control_depth)%break_requested .or. &
1710 shell%control_stack(shell%control_depth)%continue_requested) then
1711 ! Don't execute right side - break or continue was called
1712 exit_status = left_status
1713 return
1714 end if
1715 end if
1716 ! Check for function return - if requested, skip the right side
1717 if (shell%function_return_pending) then
1718 exit_status = shell%function_return_value
1719 return
1720 end if
1721 ! Check if noexec was set - if so, skip right side (set -n behavior)
1722 if (shell%option_noexec .and. .not. shell%is_interactive) then
1723 exit_status = left_status
1724 return
1725 end if
1726 if (.not. shell%running) then
1727 ! Shell is exiting (e.g., exit builtin was called)
1728 exit_status = left_status
1729 else if (associated(node%list%right)) then
1730 exit_status = execute_ast_node(node%list%right, shell)
1731 else
1732 exit_status = left_status
1733 end if
1734
1735 case(LIST_SEP_AND)
1736 ! && - Execute right only if left succeeded
1737 ! But first, handle any sourcing queued by the left side (e.g., dot command)
1738 if (shell%should_source) then
1739 call process_source_inline_ast(shell)
1740 left_status = shell%last_exit_status
1741 end if
1742 ! Check if noexec was set - if so, skip right side
1743 if (shell%option_noexec .and. .not. shell%is_interactive) then
1744 exit_status = left_status
1745 else if (left_status == 0) then
1746 if (associated(node%list%right)) then
1747 exit_status = execute_ast_node(node%list%right, shell)
1748 else
1749 exit_status = left_status
1750 end if
1751 else
1752 exit_status = left_status
1753 end if
1754 ! Mark that this result came from an AND-OR list (suppress errexit check)
1755 shell%last_from_and_or = .true.
1756
1757 case(LIST_SEP_OR)
1758 ! || - Execute right only if left failed
1759 ! But first, handle any sourcing queued by the left side (e.g., dot command)
1760 if (shell%should_source) then
1761 call process_source_inline_ast(shell)
1762 left_status = shell%last_exit_status
1763 end if
1764 ! Check if noexec was set - if so, skip right side
1765 if (shell%option_noexec .and. .not. shell%is_interactive) then
1766 exit_status = left_status
1767 else if (left_status /= 0) then
1768 if (associated(node%list%right)) then
1769 exit_status = execute_ast_node(node%list%right, shell)
1770 else
1771 exit_status = left_status
1772 end if
1773 else
1774 exit_status = left_status
1775 end if
1776 ! Mark that this result came from an AND-OR list (suppress errexit check)
1777 shell%last_from_and_or = .true.
1778
1779 case(LIST_SEP_BACKGROUND)
1780 ! & - Background jobs handled early in function (before left execution)
1781 exit_status = 0
1782
1783 case default
1784 exit_status = left_status
1785 end select
1786
1787 end function execute_list_node
1788
1789 ! =====================================
1790 ! If Statement Execution
1791 ! =====================================
1792
1793 recursive function execute_if_node(node, shell) result(exit_status)
1794 use fd_redirection, only: apply_single_redirection, save_fd_mark, restore_fds_to_mark
1795 use parser, only: expand_variables
1796 type(command_node_t), pointer, intent(in) :: node
1797 type(shell_state_t), intent(inout) :: shell
1798 integer :: exit_status, cond_status, i, fd_mark
1799 type(redirection_t) :: temp_redirect
1800 logical :: redir_success, has_redirects
1801
1802 exit_status = 0
1803
1804 if (.not. associated(node%if_stmt)) then
1805 return
1806 end if
1807
1808 ! Apply redirections for the entire if statement
1809 has_redirects = (node%num_redirects > 0)
1810 fd_mark = save_fd_mark()
1811 if (has_redirects) then
1812 block
1813 character(len=:), allocatable :: expanded_filename
1814 do i = 1, node%num_redirects
1815 temp_redirect%type = node%redirects(i)%type
1816 temp_redirect%fd = node%redirects(i)%fd
1817 temp_redirect%target_fd = node%redirects(i)%target_fd
1818 if (allocated(node%redirects(i)%filename)) then
1819 call expand_variables(trim(node%redirects(i)%filename), expanded_filename, shell)
1820 if (allocated(expanded_filename)) then
1821 allocate(temp_redirect%filename, source=trim(expanded_filename))
1822 deallocate(expanded_filename)
1823 else
1824 allocate(temp_redirect%filename, source=trim(node%redirects(i)%filename))
1825 end if
1826 end if
1827 temp_redirect%force_clobber = node%redirects(i)%force_clobber
1828
1829 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
1830 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
1831 if (.not. redir_success) then
1832 call restore_fds_to_mark(fd_mark)
1833 exit_status = 1
1834 return
1835 end if
1836 end do
1837 end block
1838 end if
1839
1840 ! Evaluate condition
1841 if (associated(node%if_stmt%condition)) then
1842 ! POSIX: Suppress errexit during condition evaluation
1843 shell%evaluating_condition = .true.
1844 cond_status = execute_ast_node(node%if_stmt%condition, shell)
1845 shell%evaluating_condition = .false.
1846 else
1847 cond_status = 1
1848 end if
1849
1850 ! Execute then or else part based on condition
1851 if (cond_status == 0) then
1852 ! Condition succeeded - execute then part
1853 if (associated(node%if_stmt%then_part)) then
1854 exit_status = execute_ast_node(node%if_stmt%then_part, shell)
1855 end if
1856 else
1857 ! Condition failed - execute else part if present
1858 if (associated(node%if_stmt%else_part)) then
1859 exit_status = execute_ast_node(node%if_stmt%else_part, shell)
1860 end if
1861 end if
1862
1863 ! Restore file descriptors if we applied redirections
1864 if (has_redirects) then
1865 call restore_fds_to_mark(fd_mark)
1866 end if
1867
1868 end function execute_if_node
1869
1870 ! =====================================
1871 ! While/Until Loop Execution
1872 ! =====================================
1873
1874 recursive function execute_while_node(node, shell) result(exit_status)
1875 use control_flow, only: push_control_block, pop_control_block, BLOCK_WHILE, BLOCK_UNTIL
1876 use fd_redirection, only: apply_single_redirection, save_fd_mark, restore_fds_to_mark
1877 use parser, only: expand_variables, read_heredoc
1878 type(command_node_t), pointer, intent(in) :: node
1879 type(shell_state_t), intent(inout) :: shell
1880 integer :: exit_status, cond_status, i, fd_mark
1881 logical :: should_continue
1882 type(redirection_t) :: temp_redirect
1883 logical :: redir_success, has_redirects
1884
1885 exit_status = 0
1886
1887 if (.not. associated(node%while_loop)) then
1888 return
1889 end if
1890
1891 ! Apply redirections for the entire while loop
1892 has_redirects = (node%num_redirects > 0)
1893 fd_mark = save_fd_mark()
1894 if (has_redirects) then
1895 block
1896 character(len=:), allocatable :: expanded_filename
1897 character(len=:), allocatable :: heredoc_content
1898 do i = 1, node%num_redirects
1899 temp_redirect%type = node%redirects(i)%type
1900 temp_redirect%fd = node%redirects(i)%fd
1901 temp_redirect%target_fd = node%redirects(i)%target_fd
1902
1903 if (temp_redirect%type == REDIR_HERE_DOC) then
1904 ! Heredoc: filename holds delimiter, retrieve content
1905 if (allocated(node%redirects(i)%filename)) then
1906 call read_heredoc( &
1907 trim(node%redirects(i)%filename), &
1908 heredoc_content, shell)
1909 if (allocated(heredoc_content)) then
1910 allocate(temp_redirect%filename, &
1911 source=heredoc_content)
1912 deallocate(heredoc_content)
1913 else
1914 allocate(temp_redirect%filename, source='')
1915 end if
1916 end if
1917 else if (allocated(node%redirects(i)%filename)) then
1918 call expand_variables( &
1919 trim(node%redirects(i)%filename), &
1920 expanded_filename, shell)
1921 if (allocated(expanded_filename)) then
1922 allocate(temp_redirect%filename, &
1923 source=trim(expanded_filename))
1924 deallocate(expanded_filename)
1925 else
1926 allocate(temp_redirect%filename, &
1927 source=trim(node%redirects(i)%filename))
1928 end if
1929 end if
1930 temp_redirect%force_clobber = &
1931 node%redirects(i)%force_clobber
1932
1933 call apply_single_redirection(temp_redirect, &
1934 redir_success, shell%option_noclobber)
1935 if (allocated(temp_redirect%filename)) &
1936 deallocate(temp_redirect%filename)
1937 if (.not. redir_success) then
1938 call restore_fds_to_mark(fd_mark)
1939 exit_status = 1
1940 return
1941 end if
1942 end do
1943 end block
1944 end if
1945
1946 ! Push loop control block so break/continue can find it
1947 if (node%while_loop%is_until) then
1948 call push_control_block(shell, BLOCK_UNTIL, .true.)
1949 else
1950 call push_control_block(shell, BLOCK_WHILE, .true.)
1951 end if
1952
1953 do
1954 ! Evaluate condition (suppress errexit during condition evaluation per POSIX)
1955 if (associated(node%while_loop%condition)) then
1956 shell%evaluating_condition = .true.
1957 cond_status = execute_ast_node(node%while_loop%condition, shell)
1958 shell%evaluating_condition = .false.
1959 else
1960 cond_status = 1
1961 end if
1962
1963 ! Determine if we should continue based on while vs until
1964 if (node%while_loop%is_until) then
1965 should_continue = (cond_status /= 0) ! until: continue while false
1966 else
1967 should_continue = (cond_status == 0) ! while: continue while true
1968 end if
1969
1970 if (.not. should_continue) exit
1971
1972 ! Execute body
1973 if (associated(node%while_loop%body)) then
1974 exit_status = execute_ast_node(node%while_loop%body, shell)
1975 end if
1976
1977 ! Check for break/continue from within the loop body
1978 if (shell%control_depth > 0) then
1979 if (shell%control_stack(shell%control_depth)%break_requested) then
1980 ! Handle multi-level break
1981 if (shell%control_stack(shell%control_depth)%break_level > 1) then
1982 ! Propagate to parent loop
1983 if (shell%control_depth > 1) then
1984 shell%control_stack(shell%control_depth - 1)%break_requested = .true.
1985 shell%control_stack(shell%control_depth - 1)%break_level = &
1986 shell%control_stack(shell%control_depth)%break_level - 1
1987 end if
1988 end if
1989 ! Clear flag and exit loop
1990 shell%control_stack(shell%control_depth)%break_requested = .false.
1991 shell%control_stack(shell%control_depth)%break_level = 0
1992 exit
1993 end if
1994
1995 if (shell%control_stack(shell%control_depth)%continue_requested) then
1996 ! Handle multi-level continue
1997 if (shell%control_stack(shell%control_depth)%continue_level > 1) then
1998 ! Propagate to parent loop
1999 if (shell%control_depth > 1) then
2000 shell%control_stack(shell%control_depth - 1)%continue_requested = .true.
2001 shell%control_stack(shell%control_depth - 1)%continue_level = &
2002 shell%control_stack(shell%control_depth)%continue_level - 1
2003 end if
2004 ! Clear and exit to outer loop
2005 shell%control_stack(shell%control_depth)%continue_requested = .false.
2006 shell%control_stack(shell%control_depth)%continue_level = 0
2007 exit
2008 else
2009 ! Clear flag and continue to next iteration
2010 shell%control_stack(shell%control_depth)%continue_requested = .false.
2011 shell%control_stack(shell%control_depth)%continue_level = 0
2012 ! Just continue the loop (next iteration)
2013 end if
2014 end if
2015 end if
2016 end do
2017
2018 ! Pop loop control block
2019 call pop_control_block(shell)
2020
2021 ! Restore file descriptors if we applied redirections
2022 if (has_redirects) then
2023 call restore_fds_to_mark(fd_mark)
2024 end if
2025
2026 end function execute_while_node
2027
2028 ! =====================================
2029 ! For Loop Execution
2030 ! =====================================
2031
2032 recursive function execute_for_node(node, shell) result(exit_status)
2033 use variables, only: set_shell_variable, get_shell_variable
2034 use control_flow, only: push_control_block, pop_control_block, BLOCK_FOR
2035 use glob, only: glob_match, has_unescaped_glob_chars
2036 use parser, only: expand_variables
2037 use fd_redirection, only: apply_single_redirection, save_fd_mark, restore_fds_to_mark
2038 type(command_node_t), pointer, intent(in) :: node
2039 type(shell_state_t), intent(inout) :: shell
2040 integer :: exit_status, i, j, glob_count, word_idx, k, split_count, fd_mark
2041 integer, parameter :: MAX_GLOB = 256, MAX_SPLIT = 256
2042 character(len=MAX_TOKEN_LEN), allocatable :: glob_matches(:)
2043 character(len=MAX_TOKEN_LEN), allocatable :: expanded_words(:)
2044 character(len=:), allocatable :: expanded_word, ifs_chars
2045 character(len=MAX_TOKEN_LEN), allocatable :: split_words(:)
2046 integer :: total_words
2047 type(redirection_t) :: temp_redirect
2048 logical :: redir_success, has_redirects
2049
2050 exit_status = 0
2051
2052 if (.not. associated(node%for_loop)) then
2053 return
2054 end if
2055
2056 ! Apply redirections for the entire for loop
2057 has_redirects = (node%num_redirects > 0)
2058 fd_mark = save_fd_mark()
2059 if (has_redirects) then
2060 block
2061 character(len=:), allocatable :: expanded_filename
2062 do i = 1, node%num_redirects
2063 temp_redirect%type = node%redirects(i)%type
2064 temp_redirect%fd = node%redirects(i)%fd
2065 temp_redirect%target_fd = node%redirects(i)%target_fd
2066 if (allocated(node%redirects(i)%filename)) then
2067 ! Expand variables in filename
2068 call expand_variables(trim(node%redirects(i)%filename), expanded_filename, shell)
2069 if (allocated(expanded_filename)) then
2070 allocate(temp_redirect%filename, source=trim(expanded_filename))
2071 deallocate(expanded_filename)
2072 else
2073 allocate(temp_redirect%filename, source=trim(node%redirects(i)%filename))
2074 end if
2075 end if
2076 temp_redirect%force_clobber = node%redirects(i)%force_clobber
2077
2078 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
2079 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
2080 if (.not. redir_success) then
2081 call restore_fds_to_mark(fd_mark)
2082 exit_status = 1
2083 return
2084 end if
2085 end do
2086 end block
2087 end if
2088
2089 ! Get IFS for word splitting
2090 ! POSIX: Empty IFS (IFS="") means no field splitting
2091 ! Unset IFS means use default (space, tab, newline)
2092 if (shell%ifs_len == 0) then
2093 ! IFS is set but empty - no splitting will occur
2094 ifs_chars = ''
2095 else if (shell%ifs_len > 0) then
2096 ! IFS is set with content - use it
2097 ifs_chars = shell%ifs(1:shell%ifs_len)
2098 else
2099 ! Default IFS
2100 ifs_chars = ' ' // achar(9) // new_line('a')
2101 end if
2102
2103 ! First, expand variables and split on IFS, then expand globs
2104 allocate(expanded_words(MAX_TOKEN_LEN), stat=i)
2105 if (i /= 0) then
2106 write(error_unit, '(A)') 'fortsh: for: allocation failure'
2107 exit_status = 1; return
2108 end if
2109 allocate(glob_matches(MAX_GLOB), stat=i)
2110 if (i /= 0) then
2111 write(error_unit, '(A)') 'fortsh: for: allocation failure'
2112 deallocate(expanded_words); exit_status = 1; return
2113 end if
2114 allocate(split_words(MAX_SPLIT), stat=i)
2115 if (i /= 0) then
2116 write(error_unit, '(A)') 'fortsh: for: allocation failure'
2117 deallocate(expanded_words); deallocate(glob_matches); exit_status = 1; return
2118 end if
2119 total_words = 0
2120
2121 ! POSIX: If 'in' is omitted (num_words == 0), iterate over positional parameters
2122 if (node%for_loop%num_words == 0) then
2123 do i = 1, shell%num_positional
2124 if (total_words < MAX_TOKEN_LEN) then
2125 total_words = total_words + 1
2126 expanded_words(total_words) = shell%positional_params(i)%str
2127 end if
2128 end do
2129 else
2130
2131 do i = 1, node%for_loop%num_words
2132 ! Special handling for quoted "$@" - each positional parameter becomes a separate word
2133 if (trim(node%for_loop%words(i)) == '$@' .and. &
2134 allocated(node%for_loop%words_was_quoted) .and. &
2135 node%for_loop%words_was_quoted(i)) then
2136 ! Quoted $@ - add each positional parameter as separate word without IFS splitting
2137 do j = 1, shell%num_positional
2138 if (total_words < MAX_TOKEN_LEN) then
2139 total_words = total_words + 1
2140 expanded_words(total_words) = shell%positional_params(j)%str
2141 end if
2142 end do
2143 cycle ! Skip normal expansion for this word
2144 end if
2145
2146 ! Special handling for quoted "${arr[@]}" - each array element becomes a separate word
2147 if (allocated(node%for_loop%words_was_quoted) .and. &
2148 i <= size(node%for_loop%words_was_quoted) .and. &
2149 node%for_loop%words_was_quoted(i)) then
2150 block
2151 use variables, only: get_array_size, &
2152 is_associative_array, get_assoc_array_keys, &
2153 get_assoc_array_value
2154 character(len=:), allocatable :: wrd
2155 character(len=256) :: arr_name
2156 character(len=256) :: akeys(200)
2157 integer :: nk, ai, bstart
2158 logical :: is_keys_expansion
2159 wrd = trim(node%for_loop%words(i))
2160 is_keys_expansion = .false.
2161 ! Match ${name[@]} or ${!name[@]} pattern
2162 if (len(wrd) > 5 .and. wrd(1:2) == '${' &
2163 .and. wrd(len(wrd)-3:) == '[@]}') &
2164 then
2165 if (wrd(3:3) == '!' .or. &
2166 (len(wrd) > 6 .and. &
2167 wrd(3:4) == '\!')) then
2168 is_keys_expansion = .true.
2169 if (wrd(3:3) == '!') then
2170 arr_name = wrd(4:len(wrd)-4)
2171 else
2172 arr_name = wrd(5:len(wrd)-4)
2173 end if
2174 else
2175 arr_name = wrd(3:len(wrd)-4)
2176 end if
2177 if (is_keys_expansion) then
2178 ! ${!name[@]} - expand keys as separate words
2179 if (is_associative_array(shell, &
2180 trim(arr_name))) then
2181 call get_assoc_array_keys(shell, &
2182 trim(arr_name), akeys, nk)
2183 do ai = 1, nk
2184 if (total_words < MAX_TOKEN_LEN) then
2185 total_words = total_words + 1
2186 expanded_words(total_words) = &
2187 akeys(ai)
2188 end if
2189 end do
2190 cycle
2191 else
2192 ! Regular array keys = indices
2193 nk = get_array_size(shell, &
2194 trim(arr_name))
2195 do ai = 0, nk - 1
2196 if (total_words < MAX_TOKEN_LEN) then
2197 block
2198 character(len=20) :: idx_str
2199 total_words = total_words + 1
2200 write(idx_str, '(i0)') ai
2201 expanded_words(total_words) = &
2202 trim(idx_str)
2203 end block
2204 end if
2205 end do
2206 if (nk > 0) cycle
2207 end if
2208 else if (is_associative_array(shell, &
2209 trim(arr_name))) then
2210 call get_assoc_array_keys(shell, &
2211 trim(arr_name), akeys, nk)
2212 do ai = 1, nk
2213 if (total_words < MAX_TOKEN_LEN) then
2214 total_words = total_words + 1
2215 expanded_words(total_words) = &
2216 get_assoc_array_value(shell, &
2217 trim(arr_name), trim(akeys(ai)))
2218 end if
2219 end do
2220 cycle
2221 else
2222 nk = get_array_size(shell, trim(arr_name))
2223 if (nk > 0) then
2224 do ai = 1, shell%num_variables
2225 if (trim(shell%variables(ai)%name) &
2226 == trim(arr_name) .and. &
2227 shell%variables(ai)%is_array) then
2228 do bstart = 1, nk
2229 if (total_words < MAX_TOKEN_LEN &
2230 .and. allocated( &
2231 shell%variables(ai) &
2232 %array_values(bstart)%str) &
2233 .and. len_trim( &
2234 shell%variables(ai) &
2235 %array_values(bstart)%str) > 0) &
2236 then
2237 total_words = total_words + 1
2238 expanded_words(total_words) = &
2239 shell%variables(ai) &
2240 %array_values(bstart)%str
2241 end if
2242 end do
2243 exit
2244 end if
2245 end do
2246 cycle
2247 end if
2248 end if
2249 end if
2250 end block
2251 end if
2252
2253 ! First expand variables (e.g., $*, $@, $var)
2254 ! Pass the quoted status so expand_variables can handle it correctly
2255 if (allocated(node%for_loop%words_was_quoted) .and. i <= size(node%for_loop%words_was_quoted)) then
2256 call expand_variables(trim(node%for_loop%words(i)), expanded_word, shell, &
2257 was_quoted_in=node%for_loop%words_was_quoted(i))
2258 else
2259 call expand_variables(trim(node%for_loop%words(i)), expanded_word, shell, was_quoted_in=.false.)
2260 end if
2261
2262 ! Split the expanded word on IFS characters ONLY if:
2263 ! 1. It was NOT originally quoted, AND
2264 ! 2. It contained a parameter expansion ($ or backtick)
2265 ! POSIX: Field splitting only occurs on results of expansions, not literal text
2266 if (allocated(node%for_loop%words_was_quoted) .and. i <= size(node%for_loop%words_was_quoted) .and. &
2267 node%for_loop%words_was_quoted(i)) then
2268 ! Word was quoted - do not split, treat as single word
2269 split_words(1) = trim(expanded_word)
2270 split_count = 1
2271 else if (index(node%for_loop%words(i), '$') > 0 .or. &
2272 index(node%for_loop%words(i), '`') > 0) then
2273 ! Word contained expansion - split on IFS
2274 call split_on_ifs(trim(expanded_word), ifs_chars, split_words, split_count)
2275 else if (index(node%for_loop%words(i), '{') > 0 .and. &
2276 index(node%for_loop%words(i), '}') > 0 .and. &
2277 allocated(expanded_word) .and. len(expanded_word) > len_trim(node%for_loop%words(i))) then
2278 ! Brace expansion produced multiple words - split on spaces
2279 call split_on_ifs(trim(expanded_word), ' ', split_words, split_count)
2280 else
2281 ! Literal word (no expansion) - do not split on IFS
2282 split_words(1) = trim(expanded_word)
2283 split_count = 1
2284 end if
2285
2286 ! Now process each split word for globs
2287 do k = 1, split_count
2288 ! Only expand globs if noglob option is NOT set (POSIX: set -f disables glob)
2289 if (.not. shell%option_noglob .and. has_unescaped_glob_chars(trim(split_words(k)))) then
2290 ! Expand the glob pattern
2291 call glob_match(trim(split_words(k)), glob_matches, glob_count)
2292 if (glob_count > 0) then
2293 ! Add all matched files
2294 do j = 1, glob_count
2295 if (total_words < MAX_TOKEN_LEN) then
2296 total_words = total_words + 1
2297 expanded_words(total_words) = glob_matches(j)
2298 end if
2299 end do
2300 else
2301 ! No matches - use the pattern literally
2302 if (total_words < MAX_TOKEN_LEN) then
2303 total_words = total_words + 1
2304 expanded_words(total_words) = split_words(k)
2305 end if
2306 end if
2307 else
2308 ! Not a glob pattern - use the word as-is
2309 if (total_words < MAX_TOKEN_LEN) then
2310 total_words = total_words + 1
2311 expanded_words(total_words) = split_words(k)
2312 end if
2313 end if
2314 end do
2315 end do
2316 end if ! End of else branch for num_words > 0
2317
2318 ! Push loop control block so break/continue can find it
2319 call push_control_block(shell, BLOCK_FOR, .true.)
2320
2321 ! Iterate over expanded words
2322 do word_idx = 1, total_words
2323 ! Set loop variable
2324 call set_shell_variable(shell, trim(node%for_loop%variable), trim(expanded_words(word_idx)), &
2325 len_trim(expanded_words(word_idx)))
2326
2327 ! Execute body
2328 if (associated(node%for_loop%body)) then
2329 exit_status = execute_ast_node(node%for_loop%body, shell)
2330 end if
2331
2332 ! Check for break/continue from within the loop body
2333 if (shell%control_depth > 0) then
2334 if (shell%control_stack(shell%control_depth)%break_requested) then
2335 ! Handle multi-level break
2336 if (shell%control_stack(shell%control_depth)%break_level > 1) then
2337 ! Propagate to parent loop
2338 if (shell%control_depth > 1) then
2339 shell%control_stack(shell%control_depth - 1)%break_requested = .true.
2340 shell%control_stack(shell%control_depth - 1)%break_level = &
2341 shell%control_stack(shell%control_depth)%break_level - 1
2342 end if
2343 end if
2344 ! Clear flag and exit loop
2345 shell%control_stack(shell%control_depth)%break_requested = .false.
2346 shell%control_stack(shell%control_depth)%break_level = 0
2347 exit
2348 end if
2349
2350 if (shell%control_stack(shell%control_depth)%continue_requested) then
2351 ! Handle multi-level continue
2352 if (shell%control_stack(shell%control_depth)%continue_level > 1) then
2353 ! Propagate to parent loop
2354 if (shell%control_depth > 1) then
2355 shell%control_stack(shell%control_depth - 1)%continue_requested = .true.
2356 shell%control_stack(shell%control_depth - 1)%continue_level = &
2357 shell%control_stack(shell%control_depth)%continue_level - 1
2358 end if
2359 ! Clear and exit to outer loop
2360 shell%control_stack(shell%control_depth)%continue_requested = .false.
2361 shell%control_stack(shell%control_depth)%continue_level = 0
2362 exit
2363 else
2364 ! Clear flag and continue to next iteration
2365 shell%control_stack(shell%control_depth)%continue_requested = .false.
2366 shell%control_stack(shell%control_depth)%continue_level = 0
2367 ! Just continue the loop (next iteration)
2368 end if
2369 end if
2370 end if
2371 end do
2372
2373 ! Pop loop control block
2374 call pop_control_block(shell)
2375
2376 ! Clean up
2377 if (allocated(expanded_words)) deallocate(expanded_words)
2378 if (allocated(glob_matches)) deallocate(glob_matches)
2379 if (allocated(split_words)) deallocate(split_words)
2380
2381 ! Restore file descriptors if we applied redirections
2382 if (has_redirects) then
2383 call restore_fds_to_mark(fd_mark)
2384 end if
2385
2386 end function execute_for_node
2387
2388 ! =====================================
2389 ! C-Style Arithmetic For Loop Execution
2390 ! =====================================
2391
2392 function execute_for_arith_node(node, shell) result(exit_status)
2393 use expansion, only: arithmetic_expansion_shell
2394 use control_flow, only: push_control_block, pop_control_block, BLOCK_FOR
2395 type(command_node_t), pointer, intent(in) :: node
2396 type(shell_state_t), intent(inout) :: shell
2397 integer :: exit_status
2398 character(len=32) :: arith_result
2399 character(len=MAX_TOKEN_LEN) :: wrapped_expr
2400 integer(kind=8) :: cond_value
2401 integer :: io_stat
2402
2403 exit_status = 0
2404 if (.not. associated(node%for_arith)) return
2405
2406 ! Evaluate init expression
2407 if (len_trim(node%for_arith%init_expr) > 0) then
2408 wrapped_expr = '$((' // trim(node%for_arith%init_expr) // '))'
2409 arith_result = arithmetic_expansion_shell(trim(wrapped_expr), shell)
2410 end if
2411
2412 ! Push loop control block for break/continue support
2413 call push_control_block(shell, BLOCK_FOR, .true.)
2414
2415 ! Loop: evaluate condition, execute body, evaluate increment
2416 do
2417 ! Evaluate condition (empty condition = infinite loop, like C)
2418 if (len_trim(node%for_arith%cond_expr) > 0) then
2419 wrapped_expr = '$((' // trim(node%for_arith%cond_expr) // '))'
2420 arith_result = arithmetic_expansion_shell(trim(wrapped_expr), shell)
2421 read(arith_result, *, iostat=io_stat) cond_value
2422 if (io_stat /= 0) cond_value = 0
2423 if (cond_value == 0) exit
2424 end if
2425
2426 ! Execute body
2427 if (associated(node%for_arith%body)) then
2428 exit_status = execute_ast_node(node%for_arith%body, shell)
2429 end if
2430
2431 ! Check for break/continue (same pattern as while loop)
2432 if (shell%control_depth > 0) then
2433 if (shell%control_stack(shell%control_depth)%break_requested) then
2434 if (shell%control_stack(shell%control_depth)%break_level > 1) then
2435 if (shell%control_depth > 1) then
2436 shell%control_stack(shell%control_depth - 1)%break_requested = .true.
2437 shell%control_stack(shell%control_depth - 1)%break_level = &
2438 shell%control_stack(shell%control_depth)%break_level - 1
2439 end if
2440 end if
2441 shell%control_stack(shell%control_depth)%break_requested = .false.
2442 shell%control_stack(shell%control_depth)%break_level = 0
2443 exit
2444 end if
2445
2446 if (shell%control_stack(shell%control_depth)%continue_requested) then
2447 if (shell%control_stack(shell%control_depth)%continue_level > 1) then
2448 if (shell%control_depth > 1) then
2449 shell%control_stack(shell%control_depth - 1)%continue_requested = .true.
2450 shell%control_stack(shell%control_depth - 1)%continue_level = &
2451 shell%control_stack(shell%control_depth)%continue_level - 1
2452 end if
2453 shell%control_stack(shell%control_depth)%continue_requested = .false.
2454 shell%control_stack(shell%control_depth)%continue_level = 0
2455 exit
2456 else
2457 shell%control_stack(shell%control_depth)%continue_requested = .false.
2458 shell%control_stack(shell%control_depth)%continue_level = 0
2459 end if
2460 end if
2461 end if
2462
2463 ! Evaluate increment expression
2464 if (len_trim(node%for_arith%incr_expr) > 0) then
2465 wrapped_expr = '$((' // trim(node%for_arith%incr_expr) // '))'
2466 arith_result = arithmetic_expansion_shell(trim(wrapped_expr), shell)
2467 end if
2468 end do
2469
2470 call pop_control_block(shell)
2471 end function execute_for_arith_node
2472
2473 ! =====================================
2474 ! Coproc Execution
2475 ! =====================================
2476
2477 function execute_coproc_node(node, shell) result(exit_status)
2478 use coprocess, only: coprocs
2479 use variables, only: set_array_element, set_shell_variable
2480 type(command_node_t), pointer, intent(in) :: node
2481 type(shell_state_t), intent(inout) :: shell
2482 integer :: exit_status
2483
2484 integer(c_int), target :: pipe_to_child(2), pipe_from_child(2)
2485 integer(c_pid_t) :: pid
2486 integer(c_int) :: ret
2487 integer :: coproc_id, status, i
2488 character(len=256) :: coproc_name
2489 character(len=16) :: fd_str, pid_str
2490
2491 exit_status = 0
2492 if (.not. associated(node%coproc)) return
2493
2494 coproc_name = trim(node%coproc%name)
2495
2496 ! Find available coprocess slot
2497 coproc_id = -1
2498 do i = 1, size(coprocs)
2499 if (.not. coprocs(i)%active) then
2500 coproc_id = i
2501 exit
2502 end if
2503 end do
2504
2505 if (coproc_id == -1) then
2506 write(error_unit, '(a)') 'coproc: maximum number of coprocesses reached'
2507 exit_status = 1
2508 return
2509 end if
2510
2511 ! Create pipes: parent writes to child stdin, reads from child stdout
2512 if (c_pipe(c_loc(pipe_to_child)) /= 0) then
2513 write(error_unit, '(a)') 'coproc: pipe creation failed'
2514 exit_status = 1
2515 return
2516 end if
2517
2518 if (c_pipe(c_loc(pipe_from_child)) /= 0) then
2519 write(error_unit, '(a)') 'coproc: pipe creation failed'
2520 ret = close(pipe_to_child(1))
2521 ret = close(pipe_to_child(2))
2522 exit_status = 1
2523 return
2524 end if
2525
2526 ! Flush before fork
2527 flush(output_unit)
2528 flush(error_unit)
2529
2530 pid = c_fork()
2531
2532 if (pid == 0) then
2533 ! Child: redirect stdin from pipe, stdout to pipe
2534 ret = c_dup2(pipe_to_child(1), int(0, c_int)) ! stdin = read end of to_child
2535 ret = close(pipe_to_child(1))
2536 ret = close(pipe_to_child(2)) ! close write end
2537
2538 ret = c_dup2(pipe_from_child(2), int(1, c_int)) ! stdout = write end of from_child
2539 ret = close(pipe_from_child(1))
2540 ret = close(pipe_from_child(2)) ! close read end
2541
2542 ! Execute the command body in the child
2543 if (associated(node%coproc%command)) then
2544 status = execute_ast_node(node%coproc%command, shell)
2545 else
2546 status = 0
2547 end if
2548 call c_exit(status)
2549
2550 else if (pid > 0) then
2551 ! Parent: close child ends of pipes
2552 ret = close(pipe_to_child(1)) ! close read end (child's stdin source)
2553 ret = close(pipe_from_child(2)) ! close write end (child's stdout dest)
2554
2555 ! Register in coprocess table
2556 coprocs(coproc_id)%name = coproc_name
2557 coprocs(coproc_id)%command = 'coproc'
2558 coprocs(coproc_id)%pid = pid
2559 coprocs(coproc_id)%write_fd = pipe_to_child(2) ! parent writes here
2560 coprocs(coproc_id)%read_fd = pipe_from_child(1) ! parent reads here
2561 coprocs(coproc_id)%active = .true.
2562 coprocs(coproc_id)%eof_reached = .false.
2563
2564 ! Set NAME_PID variable
2565 write(pid_str, '(I0)') pid
2566 call set_shell_variable(shell, trim(coproc_name) // '_PID', trim(pid_str))
2567
2568 ! Set NAME[0] = read_fd, NAME[1] = write_fd (bash convention)
2569 write(fd_str, '(I0)') pipe_from_child(1)
2570 call set_array_element(shell, trim(coproc_name), 1, trim(fd_str)) ! bash [0]
2571 write(fd_str, '(I0)') pipe_to_child(2)
2572 call set_array_element(shell, trim(coproc_name), 2, trim(fd_str)) ! bash [1]
2573
2574 if (shell%is_interactive) then
2575 write(error_unit, '(a,a,a,I0)') '[', trim(coproc_name), '] ', pid
2576 end if
2577
2578 exit_status = 0
2579 else
2580 ! Fork failed
2581 write(error_unit, '(a)') 'coproc: fork failed'
2582 ret = close(pipe_to_child(1))
2583 ret = close(pipe_to_child(2))
2584 ret = close(pipe_from_child(1))
2585 ret = close(pipe_from_child(2))
2586 exit_status = 1
2587 end if
2588 end function execute_coproc_node
2589
2590 ! =====================================
2591 ! Case Statement Execution
2592 ! =====================================
2593
2594 function execute_case_node(node, shell) result(exit_status)
2595 use variables, only: get_shell_variable
2596 use parser, only: expand_variables
2597 use fd_redirection, only: apply_single_redirection, save_fd_mark, restore_fds_to_mark
2598 type(command_node_t), pointer, intent(in) :: node
2599 type(shell_state_t), intent(inout) :: shell
2600 integer :: exit_status, i, k, fd_mark
2601 character(len=MAX_TOKEN_LEN) :: case_value
2602 integer :: case_value_len
2603 integer :: item_idx, pattern_idx
2604 logical :: matched, needs_expansion
2605 character(len=MAX_TOKEN_LEN) :: pattern
2606 character(len=:), allocatable :: expanded_pattern
2607 type(redirection_t) :: temp_redirect
2608 logical :: redir_success, has_redirects
2609
2610 exit_status = 0
2611
2612 if (.not. associated(node%case_stmt)) then
2613 return
2614 end if
2615
2616 ! Apply redirections for the entire case statement
2617 has_redirects = (node%num_redirects > 0)
2618 fd_mark = save_fd_mark()
2619 if (has_redirects) then
2620 block
2621 character(len=:), allocatable :: expanded_filename
2622 do i = 1, node%num_redirects
2623 temp_redirect%type = node%redirects(i)%type
2624 temp_redirect%fd = node%redirects(i)%fd
2625 temp_redirect%target_fd = node%redirects(i)%target_fd
2626 if (allocated(node%redirects(i)%filename)) then
2627 call expand_variables(trim(node%redirects(i)%filename), expanded_filename, shell)
2628 if (allocated(expanded_filename)) then
2629 allocate(temp_redirect%filename, source=trim(expanded_filename))
2630 deallocate(expanded_filename)
2631 else
2632 allocate(temp_redirect%filename, source=trim(node%redirects(i)%filename))
2633 end if
2634 end if
2635 temp_redirect%force_clobber = node%redirects(i)%force_clobber
2636
2637 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
2638 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
2639 if (.not. redir_success) then
2640 call restore_fds_to_mark(fd_mark)
2641 exit_status = 1
2642 return
2643 end if
2644 end do
2645 end block
2646 end if
2647
2648 ! Get the value to match (expand variables, command substitution, etc.)
2649 ! Use word_len to preserve whitespace-only values that len_trim would destroy
2650 case_value_len = node%case_stmt%word_len
2651 if (case_value_len == 0) case_value_len = len_trim(node%case_stmt%word)
2652
2653 ! Check if expansion is needed (contains $, `, or quotes)
2654 needs_expansion = .false.
2655 do k = 1, case_value_len
2656 if (node%case_stmt%word(k:k) == '$' .or. &
2657 node%case_stmt%word(k:k) == '`' .or. &
2658 node%case_stmt%word(k:k) == char(1) .or. &
2659 node%case_stmt%word(k:k) == char(2)) then
2660 needs_expansion = .true.
2661 exit
2662 end if
2663 end do
2664
2665 if (needs_expansion) then
2666 block
2667 character(len=:), allocatable :: expanded_case_value
2668 call expand_variables(node%case_stmt%word(1:case_value_len), &
2669 expanded_case_value, shell)
2670 if (allocated(expanded_case_value)) then
2671 case_value_len = len(expanded_case_value)
2672 case_value = ''
2673 if (case_value_len > 0) case_value(1:case_value_len) = expanded_case_value
2674 deallocate(expanded_case_value)
2675 else
2676 case_value = ''
2677 case_value_len = 0
2678 end if
2679 end block
2680 else
2681 case_value = ''
2682 if (case_value_len > 0) case_value(1:case_value_len) = &
2683 node%case_stmt%word(1:case_value_len)
2684 end if
2685
2686 ! Try to match against each case item
2687 do item_idx = 1, node%case_stmt%num_items
2688 matched = .false.
2689
2690 ! Check each pattern in this item
2691 do pattern_idx = 1, node%case_stmt%items(item_idx)%num_patterns
2692 pattern = trim(node%case_stmt%items(item_idx)%patterns(pattern_idx))
2693
2694 ! Expand variables in pattern (e.g., $P)
2695 call expand_variables(pattern, expanded_pattern, shell, was_quoted_in=.false.)
2696
2697 ! Match pattern using glob module (handles *, ?, [abc], [[:class:]], etc.)
2698 if (case_value_len > 0) then
2699 matched = pattern_matches_no_dotfile_check(trim(expanded_pattern), &
2700 case_value(1:case_value_len))
2701 else
2702 matched = pattern_matches_no_dotfile_check(trim(expanded_pattern), '')
2703 end if
2704
2705 if (matched) exit
2706 end do
2707
2708 ! If matched, execute the commands for this case item
2709 if (matched) then
2710 if (associated(node%case_stmt%items(item_idx)%commands)) then
2711 exit_status = execute_ast_node(node%case_stmt%items(item_idx)%commands, shell)
2712 else
2713 exit_status = 0
2714 end if
2715 exit ! Only execute first match
2716 end if
2717 end do
2718
2719 ! Restore file descriptors if we applied redirections
2720 if (has_redirects) then
2721 call restore_fds_to_mark(fd_mark)
2722 end if
2723
2724 end function execute_case_node
2725
2726 ! =====================================
2727 ! Subshell Execution
2728 ! =====================================
2729
2730 recursive function execute_subshell_node(node, shell) result(exit_status)
2731 use fd_redirection, only: apply_single_redirection
2732 use parser, only: expand_variables
2733 type(command_node_t), pointer, intent(in) :: node
2734 type(shell_state_t), intent(inout) :: shell
2735 integer :: exit_status
2736 integer(c_pid_t) :: pid
2737 integer :: status, i
2738 type(redirection_t) :: temp_redirect
2739 logical :: redir_success
2740
2741 exit_status = 0
2742
2743 if (.not. associated(node%subshell)) then
2744 return
2745 end if
2746
2747 ! Fork for subshell
2748 pid = c_fork()
2749 if (pid == 0) then
2750 ! Child process - execute commands in subshell
2751 ! POSIX: Only ignored traps (empty action) are visible in subshells
2752 ! Remove traps with commands, but keep traps with empty actions (ignore)
2753 call filter_traps_for_subshell(shell)
2754
2755 ! Apply redirections in child process
2756 if (node%num_redirects > 0) then
2757 block
2758 character(len=:), allocatable :: expanded_filename
2759 do i = 1, node%num_redirects
2760 temp_redirect%type = node%redirects(i)%type
2761 temp_redirect%fd = node%redirects(i)%fd
2762 temp_redirect%target_fd = node%redirects(i)%target_fd
2763 if (allocated(node%redirects(i)%filename)) then
2764 call expand_variables(trim(node%redirects(i)%filename), expanded_filename, shell)
2765 if (allocated(expanded_filename)) then
2766 allocate(temp_redirect%filename, source=trim(expanded_filename))
2767 deallocate(expanded_filename)
2768 else
2769 allocate(temp_redirect%filename, source=trim(node%redirects(i)%filename))
2770 end if
2771 end if
2772 temp_redirect%force_clobber = node%redirects(i)%force_clobber
2773
2774 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
2775 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
2776 if (.not. redir_success) then
2777 call c_exit(1)
2778 end if
2779 end do
2780 end block
2781 end if
2782
2783 status = execute_ast_node(node%subshell, shell)
2784 ! bash: expansion errors in subshells exit with 1, not 127
2785 if (shell%fatal_expansion_error .and. status == 127) then
2786 status = 1
2787 end if
2788 ! Fire EXIT trap before subshell exits
2789 shell%last_exit_status = status
2790 call run_exit_trap_in_subshell(shell)
2791 call c_exit(status)
2792 else if (pid > 0) then
2793 ! Parent - wait for subshell
2794 status = wait_for_process(pid)
2795 exit_status = extract_exit_status(status)
2796 else
2797 write(error_unit, '(A)') 'fortsh: fork failed for subshell'
2798 exit_status = 1
2799 end if
2800
2801 end function execute_subshell_node
2802
2803 ! =====================================
2804 ! Brace Group Execution
2805 ! =====================================
2806
2807 recursive function execute_brace_group_node(node, shell) result(exit_status)
2808 use fd_redirection, only: apply_single_redirection, save_fd_mark, restore_fds_to_mark
2809 use parser, only: expand_variables
2810 type(command_node_t), pointer, intent(in) :: node
2811 type(shell_state_t), intent(inout) :: shell
2812 integer :: exit_status
2813 integer :: i, fd_mark
2814 type(redirection_t) :: temp_redirect
2815 logical :: redir_success
2816 character(len=:), allocatable :: expanded_filename
2817
2818 exit_status = 0
2819
2820 if (.not. associated(node%subshell)) then
2821 return
2822 end if
2823
2824 ! Apply redirections if present
2825 fd_mark = save_fd_mark()
2826 if (node%num_redirects > 0) then
2827 do i = 1, node%num_redirects
2828 temp_redirect%type = node%redirects(i)%type
2829 temp_redirect%fd = node%redirects(i)%fd
2830 temp_redirect%target_fd = node%redirects(i)%target_fd
2831 if (allocated(node%redirects(i)%filename)) then
2832 ! Expand variables in redirect filename (e.g., $$ becomes PID)
2833 call expand_variables(trim(node%redirects(i)%filename), expanded_filename, shell)
2834 if (allocated(expanded_filename)) then
2835 allocate(temp_redirect%filename, source=trim(expanded_filename))
2836 deallocate(expanded_filename)
2837 else
2838 allocate(temp_redirect%filename, source=trim(node%redirects(i)%filename))
2839 end if
2840 end if
2841 temp_redirect%force_clobber = node%redirects(i)%force_clobber
2842
2843 call apply_single_redirection(temp_redirect, redir_success, shell%option_noclobber)
2844 if (allocated(temp_redirect%filename)) deallocate(temp_redirect%filename)
2845 if (.not. redir_success) then
2846 exit_status = 1
2847 call restore_fds_to_mark(fd_mark)
2848 return
2849 end if
2850 end do
2851 end if
2852
2853 ! Execute in current shell (no fork)
2854 exit_status = execute_ast_node(node%subshell, shell)
2855
2856 ! Restore file descriptors
2857 if (node%num_redirects > 0) then
2858 call restore_fds_to_mark(fd_mark)
2859 end if
2860
2861 end function execute_brace_group_node
2862
2863 ! =====================================
2864 ! Function Definition Execution
2865 ! =====================================
2866
2867 function execute_function_def(node, shell) result(exit_status)
2868 type(command_node_t), pointer, intent(in) :: node
2869 type(shell_state_t), intent(inout) :: shell
2870 integer :: exit_status
2871 integer :: func_idx, cache_idx
2872
2873 exit_status = 0
2874
2875 if (.not. associated(node%function_def)) then
2876 return
2877 end if
2878
2879 ! Store function AST body in cache
2880 cache_idx = -1
2881 do func_idx = 1, num_cached_functions
2882 if (trim(function_ast_cache(func_idx)%name) == trim(node%function_def%name)) then
2883 cache_idx = func_idx
2884 exit
2885 end if
2886 end do
2887
2888 if (cache_idx == -1) then
2889 if (num_cached_functions < size(function_ast_cache)) then
2890 ! New function - append
2891 num_cached_functions = num_cached_functions + 1
2892 cache_idx = num_cached_functions
2893 else
2894 ! Cache full - evict oldest entry (slot 1), shift down, use last slot
2895 if (associated(function_ast_cache(1)%body)) then
2896 call destroy_command_node(function_ast_cache(1)%body)
2897 end if
2898 do func_idx = 1, num_cached_functions - 1
2899 function_ast_cache(func_idx) = function_ast_cache(func_idx + 1)
2900 end do
2901 function_ast_cache(num_cached_functions)%name = ''
2902 function_ast_cache(num_cached_functions)%body => null()
2903 cache_idx = num_cached_functions
2904 end if
2905 end if
2906
2907 if (cache_idx > 0) then
2908 function_ast_cache(cache_idx)%name = trim(node%function_def%name)
2909 function_ast_cache(cache_idx)%body => node%function_def%body
2910 ! Detach body from parent AST so destroy_command_node won't free it
2911 ! The function cache now owns this subtree
2912 nullify(node%function_def%body)
2913 end if
2914
2915 ! Also register in shell state for compatibility
2916 do func_idx = 1, shell%num_functions
2917 if (trim(shell%functions(func_idx)%name) == trim(node%function_def%name)) then
2918 return ! Already registered
2919 end if
2920 end do
2921
2922 if (shell%num_functions < MAX_FUNCTIONS) then
2923 shell%num_functions = shell%num_functions + 1
2924 shell%functions(shell%num_functions)%name = trim(node%function_def%name)
2925 shell%functions(shell%num_functions)%body_lines = 1
2926 allocate(shell%functions(shell%num_functions)%body(1))
2927 shell%functions(shell%num_functions)%body(1)%str = 'AST_FUNCTION'
2928 end if
2929
2930 end function execute_function_def
2931
2932 ! =====================================
2933 ! Helper Functions
2934 ! =====================================
2935
2936 subroutine close_all_pipes(pipefd, num_pipes)
2937 integer(c_int), intent(in) :: pipefd(:,:)
2938 integer, intent(in) :: num_pipes
2939 integer :: i, ret
2940
2941 do i = 1, num_pipes
2942 ret = c_close(pipefd(1, i))
2943 ret = c_close(pipefd(2, i))
2944 end do
2945 end subroutine close_all_pipes
2946
2947 ! Trace all commands in an AST pipeline for xtrace (set -x)
2948 subroutine ast_trace_pipeline(node, shell)
2949 type(command_node_t), pointer, intent(in) :: node
2950 type(shell_state_t), intent(inout) :: shell
2951 integer :: i, j
2952 character(len=2048) :: trace_str
2953
2954 if (.not. associated(node%pipeline)) return
2955 if (.not. associated(node%pipeline%commands)) return
2956
2957 do i = 1, node%pipeline%num_commands
2958 if (node%pipeline%commands(i)%node_type /= CMD_SIMPLE) cycle
2959 if (.not. associated(node%pipeline%commands(i)%simple_cmd)) cycle
2960 if (node%pipeline%commands(i)%simple_cmd%num_words == 0) cycle
2961
2962 trace_str = ''
2963 do j = 1, node%pipeline%commands(i)%simple_cmd%num_words
2964 if (j == 1) then
2965 trace_str = trim(node%pipeline%commands(i)%simple_cmd%words(j))
2966 else
2967 trace_str = trim(trace_str) // ' ' // trim(node%pipeline%commands(i)%simple_cmd%words(j))
2968 end if
2969 end do
2970 call trace_command(shell, trim(trace_str))
2971 end do
2972 end subroutine ast_trace_pipeline
2973
2974 function wait_for_process(pid) result(status)
2975 integer(c_pid_t), intent(in) :: pid
2976 integer, target :: status
2977 integer(c_pid_t) :: result
2978
2979 status = 0
2980 result = c_waitpid(pid, c_loc(status), WUNTRACED)
2981 if (result < 0) then
2982 status = 1
2983 end if
2984 end function wait_for_process
2985
2986 function extract_exit_status(status) result(exit_code)
2987 integer, intent(in) :: status
2988 integer :: exit_code
2989
2990 ! Extract exit code from wait status
2991 exit_code = ishft(status, -8)
2992 exit_code = iand(exit_code, 255)
2993 end function extract_exit_status
2994
2995 subroutine execute_external_command(words, num_words, exit_status)
2996 character(len=*), intent(in) :: words(:)
2997 integer, intent(in) :: num_words
2998 integer, intent(out) :: exit_status
2999 character(len=MAX_PATH_LEN) :: cmd_path
3000 integer :: ret
3001
3002 interface
3003 function c_system(cmd) bind(c, name='system')
3004 import :: c_char, c_int
3005 character(kind=c_char), dimension(*) :: cmd
3006 integer(c_int) :: c_system
3007 end function
3008 end interface
3009
3010 exit_status = 0
3011
3012 if (num_words == 0) return
3013
3014 cmd_path = trim(words(1))
3015
3016 ! Try to execute command
3017 ! For now, just use system() as a placeholder
3018 ! TODO: Implement proper execvp with argv array
3019 ret = c_system(trim(cmd_path) // c_null_char)
3020 exit_status = extract_exit_status(ret)
3021
3022 end subroutine execute_external_command
3023
3024 ! Execute a pending trap command (set by signal_handling module)
3025 subroutine execute_pending_trap(shell)
3026 use grammar_parser, only: parse_command_line
3027 use command_tree, only: destroy_command_node
3028 type(shell_state_t), intent(inout) :: shell
3029 type(command_node_t), pointer :: trap_ast
3030 integer :: saved_status, trap_status
3031 logical :: saved_bypass
3032 character(len=4096) :: trap_cmd
3033
3034 ! Save the trap command and signal before clearing
3035 trap_cmd = shell%pending_trap_command
3036
3037 ! Save current exit status (traps don't affect $?)
3038 saved_status = shell%last_exit_status
3039
3040 ! Save and clear bypass_functions — trap handlers should see all functions
3041 ! even when fired inside 'command' builtin context
3042 saved_bypass = shell%bypass_functions
3043 shell%bypass_functions = .false.
3044
3045 ! Clear the pending trap
3046 shell%pending_trap_command = ''
3047 shell%pending_trap_signal = 0
3048
3049 ! Set flag to prevent recursive trap execution
3050 shell%executing_trap = .true.
3051
3052 ! Parse and execute the trap command using AST parser
3053 trap_ast => parse_command_line(trim(trap_cmd))
3054 if (associated(trap_ast)) then
3055 trap_status = execute_ast_node(trap_ast, shell)
3056 call destroy_command_node(trap_ast)
3057 end if
3058
3059 ! Clear flag to allow future trap execution
3060 shell%executing_trap = .false.
3061
3062 ! Restore bypass_functions and exit status
3063 shell%bypass_functions = saved_bypass
3064 shell%last_exit_status = saved_status
3065 end subroutine execute_pending_trap
3066
3067 ! Check for pending signals and dispatch their trap handlers
3068 subroutine dispatch_pending_signals(shell)
3069 use signal_handling, only: get_pending_trap_signals, execute_trap
3070 type(shell_state_t), intent(inout) :: shell
3071 integer :: pending_sigs(32), sig_count, si
3072 logical :: trap_executed
3073
3074 call get_pending_trap_signals(pending_sigs, sig_count)
3075 do si = 1, sig_count
3076 trap_executed = execute_trap(shell, pending_sigs(si))
3077 if (trap_executed .and. len_trim(shell%pending_trap_command) > 0) then
3078 call execute_pending_trap(shell)
3079 end if
3080 end do
3081 end subroutine dispatch_pending_signals
3082
3083 ! Process sourced files inline (for dot command in lists)
3084 subroutine process_source_inline_ast(shell)
3085 use grammar_parser, only: parse_command_line
3086 use command_tree, only: destroy_command_node
3087 use parser, only: has_unclosed_quote, ends_with_continuation_backslash, &
3088 needs_compound_continuation, remove_line_continuations
3089 type(shell_state_t), intent(inout) :: shell
3090 character(len=16384) :: input_line
3091 character(len=16384) :: continuation_line
3092 integer :: file_unit, iostat
3093 type(command_node_t), pointer :: ast_root
3094 integer :: exit_code
3095
3096 ! Reset the source flag first
3097 shell%should_source = .false.
3098
3099 ! Open file for reading
3100 open(newunit=file_unit, file=trim(shell%source_file), status='old', action='read', iostat=iostat)
3101 if (iostat /= 0) then
3102 write(error_unit, '(a)') 'source: failed to open ' // trim(shell%source_file)
3103 shell%last_exit_status = 1
3104 return
3105 end if
3106
3107 ! Increment source depth for return tracking
3108 shell%source_depth = shell%source_depth + 1
3109
3110 ! Execute each line in the file
3111 do
3112 read(file_unit, '(a)', iostat=iostat) input_line
3113 if (iostat /= 0) exit ! End of file or error
3114
3115 ! Skip empty lines and comments
3116 if (len_trim(input_line) == 0 .or. input_line(1:1) == '#') cycle
3117
3118 ! Check for unclosed quotes or backslash continuation
3119 do while (has_unclosed_quote(input_line) .or. ends_with_continuation_backslash(input_line))
3120 read(file_unit, '(a)', iostat=iostat) continuation_line
3121 if (iostat /= 0) exit
3122 input_line = trim(input_line) // char(10) // trim(continuation_line)
3123 end do
3124
3125 ! Handle line continuation (backslash-newline)
3126 input_line = remove_line_continuations(input_line)
3127
3128 ! If EOF was reached during continuation, exit
3129 if (iostat /= 0) exit
3130
3131 ! Check for unclosed compound commands (if/fi, do/done, case/esac, {/})
3132 do while (needs_compound_continuation(input_line))
3133 read(file_unit, '(a)', iostat=iostat) continuation_line
3134 if (iostat /= 0) exit
3135 input_line = trim(input_line) // char(10) // trim(continuation_line)
3136 end do
3137
3138 ! Parse and execute using AST parser
3139 ast_root => parse_command_line(trim(input_line))
3140 if (associated(ast_root)) then
3141 exit_code = execute_ast_node(ast_root, shell)
3142 shell%last_exit_status = exit_code
3143 ! Don't destroy function definitions - their AST is cached for later execution
3144 if (ast_root%node_type /= CMD_FUNCTION_DEF) then
3145 call destroy_command_node(ast_root)
3146 end if
3147 end if
3148
3149 ! Stop execution if exit command was encountered
3150 if (.not. shell%running) exit
3151
3152 ! Stop execution if return was called from sourced script
3153 if (shell%function_return_pending .and. shell%source_depth > 0) exit
3154 end do
3155
3156 ! Fire RETURN trap if set (after sourced script finishes)
3157 block
3158 use signal_handling, only: get_trap_command, TRAP_RETURN
3159 character(len=4096) :: src_return_cmd
3160 src_return_cmd = get_trap_command(shell, TRAP_RETURN)
3161 if (len_trim(src_return_cmd) > 0 .and. &
3162 .not. shell%executing_trap) then
3163 block
3164 type(command_node_t), pointer :: trap_node
3165 integer :: saved_status_src
3166 logical :: saved_bypass_src
3167 saved_status_src = shell%last_exit_status
3168 saved_bypass_src = shell%bypass_functions
3169 shell%bypass_functions = .false.
3170 shell%executing_trap = .true.
3171 trap_node => parse_command_line(trim(src_return_cmd))
3172 if (associated(trap_node)) then
3173 exit_code = execute_ast_node(trap_node, shell)
3174 call destroy_command_node(trap_node)
3175 end if
3176 shell%executing_trap = .false.
3177 shell%bypass_functions = saved_bypass_src
3178 shell%last_exit_status = saved_status_src
3179 end block
3180 end if
3181 end block
3182
3183 ! Decrement source depth
3184 shell%source_depth = shell%source_depth - 1
3185
3186 ! Clear the return flag if we're exiting due to return in sourced script
3187 if (shell%function_return_pending .and. shell%function_depth == 0) then
3188 shell%function_return_pending = .false.
3189 end if
3190
3191 close(file_unit)
3192 shell%source_file = ''
3193 end subroutine process_source_inline_ast
3194
3195 ! Unset a function from the AST cache
3196 subroutine unset_ast_function(func_name)
3197 character(len=*), intent(in) :: func_name
3198 integer :: i
3199
3200 do i = 1, num_cached_functions
3201 if (trim(function_ast_cache(i)%name) == trim(func_name)) then
3202 ! Clear this function from the cache
3203 function_ast_cache(i)%name = ''
3204 function_ast_cache(i)%body => null()
3205 exit
3206 end if
3207 end do
3208 end subroutine unset_ast_function
3209
3210 ! Check if a function exists in the AST cache
3211 function is_ast_function(func_name) result(exists)
3212 character(len=*), intent(in) :: func_name
3213 logical :: exists
3214 integer :: i
3215
3216 exists = .false.
3217 do i = 1, num_cached_functions
3218 if (trim(function_ast_cache(i)%name) == trim(func_name) .and. &
3219 len_trim(function_ast_cache(i)%name) > 0) then
3220 exists = .true.
3221 return
3222 end if
3223 end do
3224 end function is_ast_function
3225
3226 ! Split a string on IFS characters
3227 subroutine split_on_ifs(str, ifs_chars, words, word_count)
3228 character(len=*), intent(in) :: str
3229 character(len=*), intent(in) :: ifs_chars
3230 character(len=MAX_TOKEN_LEN), intent(out) :: words(:)
3231 integer, intent(out) :: word_count
3232 integer :: i, str_len, word_pos, max_words
3233 logical :: in_word
3234 character(len=MAX_TOKEN_LEN) :: current_word
3235
3236 word_count = 0
3237 current_word = ''
3238 word_pos = 0
3239 in_word = .false.
3240 str_len = len_trim(str)
3241 max_words = size(words)
3242
3243 ! Handle empty string
3244 if (str_len == 0) then
3245 return
3246 end if
3247
3248 ! Special case: empty IFS means no splitting - return entire string as one word
3249 if (len(ifs_chars) == 0) then
3250 word_count = 1
3251 words(1) = str(1:str_len)
3252 return
3253 end if
3254
3255 ! POSIX IFS splitting: non-whitespace IFS chars are explicit delimiters
3256 ! that produce empty fields between consecutive occurrences.
3257 ! IFS whitespace chars (space/tab/newline) are collapsed.
3258 block
3259 logical :: is_ifs_ws, prev_was_nonws_delim
3260 prev_was_nonws_delim = .false.
3261 do i = 1, str_len
3262 if (index(ifs_chars, str(i:i)) > 0) then
3263 is_ifs_ws = (str(i:i) == ' ' .or. str(i:i) == char(9) .or. str(i:i) == char(10))
3264 if (in_word) then
3265 ! End current word
3266 word_count = word_count + 1
3267 if (word_count <= max_words) then
3268 words(word_count) = current_word(1:word_pos)
3269 end if
3270 current_word = ''
3271 word_pos = 0
3272 in_word = .false.
3273 else if (.not. is_ifs_ws .and. prev_was_nonws_delim) then
3274 ! Consecutive non-whitespace IFS chars produce empty fields
3275 word_count = word_count + 1
3276 if (word_count <= max_words) then
3277 words(word_count) = ''
3278 end if
3279 end if
3280 prev_was_nonws_delim = .not. is_ifs_ws
3281 else
3282 ! Non-IFS character - add to current word
3283 word_pos = word_pos + 1
3284 current_word(word_pos:word_pos) = str(i:i)
3285 in_word = .true.
3286 prev_was_nonws_delim = .false.
3287 end if
3288 end do
3289 end block
3290
3291 ! Add final word if any
3292 if (in_word) then
3293 word_count = word_count + 1
3294 if (word_count <= max_words) then
3295 words(word_count) = current_word(1:word_pos)
3296 end if
3297 end if
3298 end subroutine split_on_ifs
3299
3300 ! Pre-expand all simple command words in a pipeline before forking
3301 ! POSIX: Expansion errors should go to parent shell's stderr
3302 subroutine pre_expand_pipeline(node, shell)
3303 use pipeline_helpers, only: expand_tokens
3304 type(command_node_t), pointer, intent(in) :: node
3305 type(shell_state_t), intent(inout) :: shell
3306 integer :: i, j
3307 type(command_t) :: temp_cmd
3308
3309 if (.not. associated(node%pipeline)) return
3310 if (.not. associated(node%pipeline%commands)) return
3311
3312 do i = 1, node%pipeline%num_commands
3313 if (node%pipeline%commands(i)%node_type /= CMD_SIMPLE) cycle
3314 if (.not. associated(node%pipeline%commands(i)%simple_cmd)) cycle
3315
3316 ! Build temporary command structure for expansion
3317 temp_cmd%num_tokens = node%pipeline%commands(i)%simple_cmd%num_words
3318 if (temp_cmd%num_tokens == 0) cycle
3319
3320 allocate(character(len=MAX_TOKEN_LEN) :: temp_cmd%tokens(temp_cmd%num_tokens))
3321 allocate(temp_cmd%token_quoted(temp_cmd%num_tokens))
3322 allocate(temp_cmd%token_escaped(temp_cmd%num_tokens))
3323 allocate(temp_cmd%token_quote_type(temp_cmd%num_tokens))
3324 allocate(temp_cmd%token_lengths(temp_cmd%num_tokens))
3325
3326 ! Copy words to temp command
3327 do j = 1, temp_cmd%num_tokens
3328 ! Preserve whitespace for quoted tokens by using word_lengths
3329 if (allocated(node%pipeline%commands(i)%simple_cmd%word_was_quoted) .and. &
3330 allocated(node%pipeline%commands(i)%simple_cmd%word_lengths)) then
3331 if (node%pipeline%commands(i)%simple_cmd%word_was_quoted(j)) then
3332 temp_cmd%tokens(j) = node%pipeline%commands(i)%simple_cmd%words(j)( &
3333 1:node%pipeline%commands(i)%simple_cmd%word_lengths(j))
3334 temp_cmd%token_lengths(j) = node%pipeline%commands(i)%simple_cmd%word_lengths(j)
3335 else
3336 temp_cmd%tokens(j) = trim(node%pipeline%commands(i)%simple_cmd%words(j))
3337 temp_cmd%token_lengths(j) = len_trim(node%pipeline%commands(i)%simple_cmd%words(j))
3338 end if
3339 else
3340 temp_cmd%tokens(j) = trim(node%pipeline%commands(i)%simple_cmd%words(j))
3341 temp_cmd%token_lengths(j) = len_trim(node%pipeline%commands(i)%simple_cmd%words(j))
3342 end if
3343 if (allocated(node%pipeline%commands(i)%simple_cmd%word_was_quoted)) then
3344 temp_cmd%token_quoted(j) = node%pipeline%commands(i)%simple_cmd%word_was_quoted(j)
3345 else
3346 temp_cmd%token_quoted(j) = .false.
3347 end if
3348 if (allocated(node%pipeline%commands(i)%simple_cmd%word_was_escaped)) then
3349 temp_cmd%token_escaped(j) = node%pipeline%commands(i)%simple_cmd%word_was_escaped(j)
3350 else
3351 temp_cmd%token_escaped(j) = .false.
3352 end if
3353 if (allocated(node%pipeline%commands(i)%simple_cmd%word_quote_type)) then
3354 temp_cmd%token_quote_type(j) = node%pipeline%commands(i)%simple_cmd%word_quote_type(j)
3355 else
3356 temp_cmd%token_quote_type(j) = QUOTE_NONE
3357 end if
3358 end do
3359
3360 ! Expand tokens (errors go to parent stderr)
3361 call expand_tokens(temp_cmd, shell)
3362
3363 ! Move expanded tokens into AST node — avoids allocating + copying large arrays
3364 if (allocated(node%pipeline%commands(i)%simple_cmd%words)) &
3365 deallocate(node%pipeline%commands(i)%simple_cmd%words)
3366 if (allocated(node%pipeline%commands(i)%simple_cmd%word_lengths)) &
3367 deallocate(node%pipeline%commands(i)%simple_cmd%word_lengths)
3368 if (allocated(node%pipeline%commands(i)%simple_cmd%word_was_quoted)) &
3369 deallocate(node%pipeline%commands(i)%simple_cmd%word_was_quoted)
3370 if (allocated(node%pipeline%commands(i)%simple_cmd%word_was_escaped)) &
3371 deallocate(node%pipeline%commands(i)%simple_cmd%word_was_escaped)
3372 if (allocated(node%pipeline%commands(i)%simple_cmd%word_quote_type)) &
3373 deallocate(node%pipeline%commands(i)%simple_cmd%word_quote_type)
3374
3375 call move_alloc(temp_cmd%tokens, node%pipeline%commands(i)%simple_cmd%words)
3376 call move_alloc(temp_cmd%token_lengths, node%pipeline%commands(i)%simple_cmd%word_lengths)
3377 call move_alloc(temp_cmd%token_quoted, node%pipeline%commands(i)%simple_cmd%word_was_quoted)
3378 call move_alloc(temp_cmd%token_escaped, node%pipeline%commands(i)%simple_cmd%word_was_escaped)
3379 call move_alloc(temp_cmd%token_quote_type, node%pipeline%commands(i)%simple_cmd%word_quote_type)
3380 node%pipeline%commands(i)%simple_cmd%num_words = temp_cmd%num_tokens
3381
3382 ! Mark as pre-expanded so executor skips expansion
3383 node%pipeline%commands(i)%simple_cmd%pre_expanded = .true.
3384 end do
3385 end subroutine pre_expand_pipeline
3386
3387 ! Check if a string is a valid shell variable name for assignment
3388 ! POSIX: name must start with letter or underscore, followed by letters, digits, or underscores
3389 function is_valid_assignment_name(name) result(valid)
3390 character(len=*), intent(in) :: name
3391 logical :: valid
3392 integer :: i, name_len
3393 character :: ch
3394
3395 valid = .false.
3396 name_len = len_trim(name)
3397
3398 if (name_len == 0) return
3399
3400 ! First character must be letter or underscore
3401 ch = name(1:1)
3402 if (.not. ((ch >= 'a' .and. ch <= 'z') .or. &
3403 (ch >= 'A' .and. ch <= 'Z') .or. &
3404 ch == '_')) then
3405 return
3406 end if
3407
3408 ! Remaining characters must be letter, digit, or underscore
3409 do i = 2, name_len
3410 ch = name(i:i)
3411 if (.not. ((ch >= 'a' .and. ch <= 'z') .or. &
3412 (ch >= 'A' .and. ch <= 'Z') .or. &
3413 (ch >= '0' .and. ch <= '9') .or. &
3414 ch == '_')) then
3415 return
3416 end if
3417 end do
3418
3419 valid = .true.
3420 end function is_valid_assignment_name
3421
3422 ! Mark traps as inherited for subshell: traps remain visible for listing
3423 ! but will not be executed when the subshell exits
3424 ! POSIX: `trap` command should show parent's traps, but they don't execute in subshell
3425 subroutine filter_traps_for_subshell(shell)
3426 type(shell_state_t), intent(inout) :: shell
3427 integer :: i
3428
3429 do i = 1, shell%num_traps
3430 if (shell%traps(i)%active) then
3431 ! Mark all traps with commands as inherited (visible but not executed)
3432 ! Empty command traps (ignore) remain effective
3433 if (len_trim(shell%traps(i)%command) > 0) then
3434 shell%traps(i)%inherited = .true.
3435 end if
3436 end if
3437 end do
3438 end subroutine filter_traps_for_subshell
3439
3440 subroutine run_exit_trap_in_subshell(shell)
3441 use trap_dispatch, only: eval_trap_string
3442 type(shell_state_t), intent(inout) :: shell
3443 integer :: i, trap_exit_code
3444
3445 if (shell%executing_trap .or. shell%exit_trap_executed) return
3446
3447 ! Find EXIT trap (signal 0) that was set in this subshell
3448 do i = 1, shell%num_traps
3449 if (shell%traps(i)%signal == 0 .and. &
3450 shell%traps(i)%active .and. &
3451 .not. shell%traps(i)%inherited .and. &
3452 len_trim(shell%traps(i)%command) > 0) then
3453 shell%exit_trap_executed = .true.
3454 shell%executing_trap = .true.
3455 call eval_trap_string( &
3456 trim(shell%traps(i)%command), &
3457 shell, trap_exit_code)
3458 shell%executing_trap = .false.
3459 return
3460 end if
3461 end do
3462 end subroutine run_exit_trap_in_subshell
3463
3464 end module ast_executor
3465