| 1 | ! ============================================================================== |
| 2 | ! Module: executor (Extended with job control) |
| 3 | ! ============================================================================== |
| 4 | module executor |
| 5 | use shell_types |
| 6 | use system_interface |
| 7 | use builtin_interface |
| 8 | use parser |
| 9 | use job_control |
| 10 | use pipeline_helpers, only: expand_tokens, expand_command_globs, process_command_escapes |
| 11 | use variables, only: var_set_shell_variable => set_shell_variable, set_array_variable, set_array_element, & |
| 12 | get_shell_variable |
| 13 | use control_flow |
| 14 | use error_handling |
| 15 | use performance |
| 16 | use aliases, only: expand_alias, is_alias, get_alias |
| 17 | use shell_options |
| 18 | use signal_handling, only: execute_trap, TRAP_DEBUG, TRAP_ERR |
| 19 | use better_errors |
| 20 | use completion, only: register_completion_executor, completion_func_executor_t |
| 21 | use iso_fortran_env, only: error_unit, input_unit |
| 22 | use iso_c_binding |
| 23 | implicit none |
| 24 | |
| 25 | public :: init_completion_executor |
| 26 | |
| 27 | contains |
| 28 | |
| 29 | subroutine execute_pipeline(pipeline, shell, original_input) |
| 30 | type(pipeline_t), intent(inout) :: pipeline |
| 31 | type(shell_state_t), intent(inout) :: shell |
| 32 | character(len=*), intent(in) :: original_input |
| 33 | |
| 34 | integer :: i |
| 35 | logical :: should_continue |
| 36 | |
| 37 | should_continue = .true. |
| 38 | i = 1 |
| 39 | |
| 40 | do while (i <= pipeline%num_commands .and. should_continue) |
| 41 | select case(pipeline%commands(i)%separator) |
| 42 | case(SEP_PIPE) |
| 43 | call execute_pipe_chain(pipeline, i, shell, original_input) |
| 44 | call check_errexit(shell, shell%last_exit_status) |
| 45 | ! Check if shell should exit (e.g., due to ${VAR?error}) |
| 46 | if (.not. shell%running) exit |
| 47 | do while (i <= pipeline%num_commands) |
| 48 | if (pipeline%commands(i)%separator /= SEP_PIPE) exit |
| 49 | i = i + 1 |
| 50 | end do |
| 51 | ! Skip the last command in the pipeline (it has non-PIPE separator) |
| 52 | i = i + 1 |
| 53 | |
| 54 | case(SEP_SEMICOLON, SEP_NONE) |
| 55 | call execute_single(pipeline%commands(i), shell, original_input) |
| 56 | ! NOTE: Sourcing is now handled at the AST level (in execute_ast) or by the caller |
| 57 | ! We don't process sourced files inline here to avoid double-processing |
| 58 | call check_errexit(shell, shell%last_exit_status) |
| 59 | ! Check if shell should exit (e.g., due to ${VAR?error}) |
| 60 | if (.not. shell%running) exit |
| 61 | i = i + 1 |
| 62 | |
| 63 | case(SEP_AND) |
| 64 | call execute_single(pipeline%commands(i), shell, original_input) |
| 65 | should_continue = (shell%last_exit_status == 0) |
| 66 | ! POSIX: errexit should be ignored in AND-OR lists |
| 67 | ! Check if shell should exit (e.g., due to ${VAR?error}) |
| 68 | if (.not. shell%running) exit |
| 69 | i = i + 1 |
| 70 | |
| 71 | case(SEP_OR) |
| 72 | call execute_single(pipeline%commands(i), shell, original_input) |
| 73 | should_continue = (shell%last_exit_status /= 0) |
| 74 | ! POSIX: errexit should be ignored in AND-OR lists |
| 75 | ! Check if shell should exit (e.g., due to ${VAR?error}) |
| 76 | if (.not. shell%running) exit |
| 77 | i = i + 1 |
| 78 | end select |
| 79 | end do |
| 80 | end subroutine |
| 81 | |
| 82 | ! DEPRECATED: Legacy pipeline execution path, only used by FORTSH_USE_OLD_PARSER=1. |
| 83 | ! The AST executor (ast_executor.f90::execute_pipeline_node) is the primary pipeline |
| 84 | ! implementation with full feature parity. This subroutine will be removed when the |
| 85 | ! legacy parser path is retired. |
| 86 | subroutine execute_pipe_chain(pipeline, start_idx, shell, original_input) |
| 87 | type(pipeline_t), intent(inout) :: pipeline |
| 88 | integer, intent(in) :: start_idx |
| 89 | type(shell_state_t), intent(inout) :: shell |
| 90 | character(len=*), intent(in) :: original_input |
| 91 | |
| 92 | integer :: i, j, pipe_count, end_idx |
| 93 | integer(c_int), allocatable :: pipefd(:,:) |
| 94 | integer(c_pid_t), allocatable :: pids(:) |
| 95 | integer(c_pid_t) :: pgid |
| 96 | integer :: ret, job_id |
| 97 | logical :: foreground |
| 98 | type(c_funptr) :: old_handler |
| 99 | type(pipeline_t) :: group_pipeline |
| 100 | integer :: k |
| 101 | character(len=2048) :: reconstructed_cmd |
| 102 | |
| 103 | ! Count pipes in chain |
| 104 | pipe_count = 0 |
| 105 | end_idx = start_idx |
| 106 | do i = start_idx, pipeline%num_commands - 1 |
| 107 | if (pipeline%commands(i)%separator == SEP_PIPE) then |
| 108 | pipe_count = pipe_count + 1 |
| 109 | end_idx = i + 1 |
| 110 | else |
| 111 | exit |
| 112 | end if |
| 113 | end do |
| 114 | |
| 115 | if (pipe_count == 0) then |
| 116 | call execute_single(pipeline%commands(start_idx), shell, original_input) |
| 117 | return |
| 118 | end if |
| 119 | |
| 120 | foreground = .not. pipeline%commands(end_idx)%background |
| 121 | |
| 122 | allocate(pipefd(2, pipe_count)) |
| 123 | allocate(pids(pipe_count + 1)) |
| 124 | |
| 125 | ! Create all pipes |
| 126 | do i = 1, pipe_count |
| 127 | if (.not. create_pipe(pipefd(1,i), pipefd(2,i))) then |
| 128 | write(error_unit, '(a)') 'Error: Failed to create pipe' |
| 129 | shell%last_exit_status = 1 |
| 130 | return |
| 131 | end if |
| 132 | end do |
| 133 | |
| 134 | pgid = 0 |
| 135 | |
| 136 | ! Trace all pipeline commands BEFORE forking (so order is deterministic) |
| 137 | if (shell%option_xtrace) then |
| 138 | do i = start_idx, end_idx |
| 139 | if (pipeline%commands(i)%num_tokens > 0) then |
| 140 | call reconstruct_command_from_tokens(pipeline%commands(i), reconstructed_cmd) |
| 141 | call trace_command(shell, trim(reconstructed_cmd)) |
| 142 | end if |
| 143 | end do |
| 144 | end if |
| 145 | |
| 146 | ! Flush all output before forking to prevent buffer duplication |
| 147 | flush(output_unit) |
| 148 | flush(error_unit) |
| 149 | |
| 150 | ! Fork all processes |
| 151 | do i = start_idx, end_idx |
| 152 | pids(i - start_idx + 1) = c_fork() |
| 153 | |
| 154 | if (pids(i - start_idx + 1) == 0) then |
| 155 | ! Child process |
| 156 | |
| 157 | ! Set process group |
| 158 | if (pgid == 0) pgid = c_getpid() |
| 159 | ret = c_setpgid(0, pgid) |
| 160 | |
| 161 | ! Reset signal handlers to default |
| 162 | old_handler = c_signal(SIGINT, c_null_funptr) |
| 163 | old_handler = c_signal(SIGPIPE, c_null_funptr) |
| 164 | old_handler = c_signal(SIGTSTP, c_null_funptr) |
| 165 | old_handler = c_signal(SIGTTIN, c_null_funptr) |
| 166 | old_handler = c_signal(SIGTTOU, c_null_funptr) |
| 167 | |
| 168 | ! Set up pipes |
| 169 | if (i > start_idx) then |
| 170 | ret = c_dup2(pipefd(1, i - start_idx), STDIN_FD) |
| 171 | end if |
| 172 | |
| 173 | if (i < end_idx) then |
| 174 | ret = c_dup2(pipefd(2, i - start_idx + 1), STDOUT_FD) |
| 175 | end if |
| 176 | |
| 177 | ! Close all pipe FDs |
| 178 | do j = 1, pipe_count |
| 179 | ret = c_close(pipefd(1, j)) |
| 180 | ret = c_close(pipefd(2, j)) |
| 181 | end do |
| 182 | |
| 183 | ! Handle here document |
| 184 | call handle_heredoc(pipeline%commands(i), shell) |
| 185 | |
| 186 | ! Handle command groups { cmd1; cmd2; } |
| 187 | if (pipeline%commands(i)%is_command_group .and. allocated(pipeline%commands(i)%group_content)) then |
| 188 | ! Parse the group content as a pipeline and execute it |
| 189 | call parse_pipeline(pipeline%commands(i)%group_content, group_pipeline) |
| 190 | if (group_pipeline%num_commands > 0) then |
| 191 | call execute_pipeline(group_pipeline, shell, pipeline%commands(i)%group_content) |
| 192 | ! Clean up |
| 193 | do k = 1, group_pipeline%num_commands |
| 194 | if (allocated(group_pipeline%commands(k)%tokens)) deallocate(group_pipeline%commands(k)%tokens) |
| 195 | if (allocated(group_pipeline%commands(k)%input_file)) deallocate(group_pipeline%commands(k)%input_file) |
| 196 | if (allocated(group_pipeline%commands(k)%output_file)) deallocate(group_pipeline%commands(k)%output_file) |
| 197 | if (allocated(group_pipeline%commands(k)%error_file)) deallocate(group_pipeline%commands(k)%error_file) |
| 198 | if (allocated(group_pipeline%commands(k)%heredoc_delimiter)) deallocate(group_pipeline%commands(k)%heredoc_delimiter) |
| 199 | if (allocated(group_pipeline%commands(k)%heredoc_content)) deallocate(group_pipeline%commands(k)%heredoc_content) |
| 200 | if (allocated(group_pipeline%commands(k)%here_string)) deallocate(group_pipeline%commands(k)%here_string) |
| 201 | if (allocated(group_pipeline%commands(k)%group_content)) deallocate(group_pipeline%commands(k)%group_content) |
| 202 | end do |
| 203 | if (allocated(group_pipeline%commands)) deallocate(group_pipeline%commands) |
| 204 | end if |
| 205 | call c_exit(int(shell%last_exit_status, c_int)) |
| 206 | end if |
| 207 | |
| 208 | ! Handle subshells ( cmd1; cmd2 ) |
| 209 | if (pipeline%commands(i)%is_subshell .and. allocated(pipeline%commands(i)%subshell_content)) then |
| 210 | ! Parse the subshell content as a pipeline and execute it |
| 211 | call parse_pipeline(pipeline%commands(i)%subshell_content, group_pipeline) |
| 212 | if (group_pipeline%num_commands > 0) then |
| 213 | call execute_pipeline(group_pipeline, shell, pipeline%commands(i)%subshell_content) |
| 214 | ! Clean up |
| 215 | do k = 1, group_pipeline%num_commands |
| 216 | if (allocated(group_pipeline%commands(k)%tokens)) deallocate(group_pipeline%commands(k)%tokens) |
| 217 | if (allocated(group_pipeline%commands(k)%input_file)) deallocate(group_pipeline%commands(k)%input_file) |
| 218 | if (allocated(group_pipeline%commands(k)%output_file)) deallocate(group_pipeline%commands(k)%output_file) |
| 219 | if (allocated(group_pipeline%commands(k)%error_file)) deallocate(group_pipeline%commands(k)%error_file) |
| 220 | if (allocated(group_pipeline%commands(k)%heredoc_delimiter)) deallocate(group_pipeline%commands(k)%heredoc_delimiter) |
| 221 | if (allocated(group_pipeline%commands(k)%heredoc_content)) deallocate(group_pipeline%commands(k)%heredoc_content) |
| 222 | if (allocated(group_pipeline%commands(k)%here_string)) deallocate(group_pipeline%commands(k)%here_string) |
| 223 | if (allocated(group_pipeline%commands(k)%group_content)) deallocate(group_pipeline%commands(k)%group_content) |
| 224 | if (allocated(group_pipeline%commands(k)%subshell_content)) deallocate(group_pipeline%commands(k)%subshell_content) |
| 225 | end do |
| 226 | if (allocated(group_pipeline%commands)) deallocate(group_pipeline%commands) |
| 227 | end if |
| 228 | call c_exit(int(shell%last_exit_status, c_int)) |
| 229 | end if |
| 230 | |
| 231 | ! Check if we have tokens to process |
| 232 | if (pipeline%commands(i)%num_tokens == 0) then |
| 233 | ! No tokens (shouldn't happen after command group handling) |
| 234 | write(error_unit, '(a)') 'Error: command has no tokens' |
| 235 | call c_exit(1) |
| 236 | end if |
| 237 | |
| 238 | ! Expand variables and execute (unless pre-expanded in pipeline) |
| 239 | if (.not. pipeline%commands(i)%skip_expansion) then |
| 240 | call expand_tokens(pipeline%commands(i), shell) |
| 241 | end if |
| 242 | |
| 243 | ! Expand glob patterns |
| 244 | call expand_command_globs(pipeline%commands(i), shell) |
| 245 | |
| 246 | ! Process backslash escapes AFTER glob expansion |
| 247 | call process_command_escapes(pipeline%commands(i)) |
| 248 | |
| 249 | ! Handle eval builtin directly (to avoid circular dependency) |
| 250 | ! Removed special handling for eval - it's now a regular builtin |
| 251 | if (is_builtin(pipeline%commands(i)%tokens(1))) then |
| 252 | ! Builtins in pipes need redirections applied |
| 253 | call setup_redirections(pipeline%commands(i), shell) |
| 254 | call execute_builtin(pipeline%commands(i), shell) |
| 255 | call c_exit(int(shell%last_exit_status, c_int)) |
| 256 | else |
| 257 | call setup_redirections(pipeline%commands(i), shell) |
| 258 | call exec_child(pipeline%commands(i)%tokens, pipeline%commands(i)%num_tokens) |
| 259 | call c_exit(127) |
| 260 | end if |
| 261 | else if (pids(i - start_idx + 1) > 0) then |
| 262 | ! Parent: set process group |
| 263 | if (pgid == 0) pgid = pids(1) |
| 264 | ret = c_setpgid(pids(i - start_idx + 1), pgid) |
| 265 | end if |
| 266 | end do |
| 267 | |
| 268 | ! Parent: close all pipes |
| 269 | do i = 1, pipe_count |
| 270 | ret = c_close(pipefd(1, i)) |
| 271 | ret = c_close(pipefd(2, i)) |
| 272 | end do |
| 273 | |
| 274 | ! Add job to job list |
| 275 | if (.not. foreground) then |
| 276 | job_id = add_job(shell, pgid, original_input, .false.) |
| 277 | ! Only print job notification in interactive mode |
| 278 | if (shell%is_interactive) then |
| 279 | write(output_unit, '(a,i0,a,i0)') '[', job_id, '] ', pgid |
| 280 | end if |
| 281 | shell%last_pid = pgid |
| 282 | ! Set $! to last process in background pipeline |
| 283 | shell%last_bg_pid = pids(pipe_count + 1) |
| 284 | else if (shell%is_interactive) then |
| 285 | ! Give terminal to job |
| 286 | ret = c_tcsetpgrp(shell%shell_terminal, pgid) |
| 287 | end if |
| 288 | |
| 289 | ! Wait for all children (if foreground) |
| 290 | if (foreground) then |
| 291 | call wait_for_pipeline(shell, pids, pipe_count + 1) |
| 292 | |
| 293 | ! Take back terminal |
| 294 | if (shell%is_interactive) then |
| 295 | ret = c_tcsetpgrp(shell%shell_terminal, shell%shell_pgid) |
| 296 | end if |
| 297 | end if |
| 298 | |
| 299 | deallocate(pipefd) |
| 300 | deallocate(pids) |
| 301 | end subroutine |
| 302 | |
| 303 | ! Wait for pipeline processes with POSIX-compliant exit status handling |
| 304 | subroutine wait_for_pipeline(shell, pids, num_processes) |
| 305 | type(shell_state_t), intent(inout) :: shell |
| 306 | integer(c_pid_t), intent(in) :: pids(:) |
| 307 | integer, intent(in) :: num_processes |
| 308 | |
| 309 | integer(c_int), target :: status |
| 310 | integer :: i, ret |
| 311 | integer, allocatable :: exit_statuses(:) |
| 312 | |
| 313 | allocate(exit_statuses(num_processes)) |
| 314 | |
| 315 | ! Wait for all processes and collect their exit statuses |
| 316 | do i = 1, num_processes |
| 317 | ret = c_waitpid(pids(i), c_loc(status), WUNTRACED) |
| 318 | if (ret > 0) then |
| 319 | if (WIFEXITED(status)) then |
| 320 | exit_statuses(i) = WEXITSTATUS(status) |
| 321 | else if (WIFSIGNALED(status)) then |
| 322 | exit_statuses(i) = 128 + WTERMSIG(status) |
| 323 | else |
| 324 | exit_statuses(i) = 1 |
| 325 | end if |
| 326 | else |
| 327 | exit_statuses(i) = 1 |
| 328 | end if |
| 329 | end do |
| 330 | |
| 331 | ! Set exit status according to POSIX/bash rules |
| 332 | if (shell%option_pipefail) then |
| 333 | ! pipefail: return rightmost non-zero exit status (bash-correct semantics) |
| 334 | shell%last_exit_status = 0 |
| 335 | do i = num_processes, 1, -1 |
| 336 | if (exit_statuses(i) /= 0) then |
| 337 | shell%last_exit_status = exit_statuses(i) |
| 338 | exit |
| 339 | end if |
| 340 | end do |
| 341 | else |
| 342 | ! Normal: return exit status of last (rightmost) command |
| 343 | shell%last_exit_status = exit_statuses(num_processes) |
| 344 | end if |
| 345 | |
| 346 | deallocate(exit_statuses) |
| 347 | end subroutine |
| 348 | |
| 349 | recursive subroutine execute_single(cmd, shell, original_input) |
| 350 | use control_flow, only: capture_loop_command, is_control_flow_keyword |
| 351 | type(command_t), intent(inout) :: cmd |
| 352 | type(shell_state_t), intent(inout) :: shell |
| 353 | character(len=*), intent(in) :: original_input |
| 354 | logical :: should_execute, trap_executed, negate_exit_status |
| 355 | integer(int64) :: exec_start_time |
| 356 | integer :: i |
| 357 | character(len=256) :: reconstructed_cmd |
| 358 | character(len=MAX_TOKEN_LEN), allocatable :: temp_tokens(:) |
| 359 | type(pipeline_t) :: pipeline |
| 360 | |
| 361 | ! Start performance timing |
| 362 | call start_timer('execute_single', exec_start_time) |
| 363 | |
| 364 | ! Handle subshells ( cmd1; cmd2 ) |
| 365 | if (cmd%is_subshell .and. allocated(cmd%subshell_content)) then |
| 366 | ! Fork a subshell and execute commands in it |
| 367 | call execute_subshell(cmd%subshell_content, shell, original_input) |
| 368 | ! End performance timing |
| 369 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 370 | return |
| 371 | end if |
| 372 | |
| 373 | ! Handle command groups { cmd1; cmd2; } |
| 374 | if (cmd%is_command_group .and. allocated(cmd%group_content)) then |
| 375 | ! Parse the group content as a pipeline and execute it |
| 376 | call parse_pipeline(cmd%group_content, pipeline) |
| 377 | if (pipeline%num_commands > 0) then |
| 378 | call execute_pipeline(pipeline, shell, cmd%group_content) |
| 379 | ! Clean up |
| 380 | do i = 1, pipeline%num_commands |
| 381 | if (allocated(pipeline%commands(i)%tokens)) deallocate(pipeline%commands(i)%tokens) |
| 382 | if (allocated(pipeline%commands(i)%input_file)) deallocate(pipeline%commands(i)%input_file) |
| 383 | if (allocated(pipeline%commands(i)%output_file)) deallocate(pipeline%commands(i)%output_file) |
| 384 | if (allocated(pipeline%commands(i)%error_file)) deallocate(pipeline%commands(i)%error_file) |
| 385 | if (allocated(pipeline%commands(i)%heredoc_delimiter)) deallocate(pipeline%commands(i)%heredoc_delimiter) |
| 386 | if (allocated(pipeline%commands(i)%heredoc_content)) deallocate(pipeline%commands(i)%heredoc_content) |
| 387 | if (allocated(pipeline%commands(i)%here_string)) deallocate(pipeline%commands(i)%here_string) |
| 388 | if (allocated(pipeline%commands(i)%group_content)) deallocate(pipeline%commands(i)%group_content) |
| 389 | end do |
| 390 | if (allocated(pipeline%commands)) deallocate(pipeline%commands) |
| 391 | end if |
| 392 | ! End performance timing |
| 393 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 394 | return |
| 395 | end if |
| 396 | |
| 397 | if (cmd%num_tokens == 0) return |
| 398 | |
| 399 | ! Check for empty command (e.g., from empty command substitution result) |
| 400 | if (len_trim(cmd%tokens(1)) == 0) then |
| 401 | ! Empty command - nothing to execute |
| 402 | ! Note: exit status from command substitution is preserved |
| 403 | return |
| 404 | end if |
| 405 | |
| 406 | ! Handle negation operator (!) |
| 407 | negate_exit_status = .false. |
| 408 | |
| 409 | ! Check if first token is the negation operator |
| 410 | if (trim(cmd%tokens(1)) == '!') then |
| 411 | negate_exit_status = .true. |
| 412 | |
| 413 | ! Remove the ! from tokens and shift everything left |
| 414 | if (cmd%num_tokens > 1) then |
| 415 | allocate(temp_tokens(cmd%num_tokens - 1)) |
| 416 | do i = 2, cmd%num_tokens |
| 417 | temp_tokens(i - 1) = cmd%tokens(i) |
| 418 | end do |
| 419 | |
| 420 | ! Replace cmd%tokens with shifted tokens |
| 421 | deallocate(cmd%tokens) |
| 422 | allocate(character(len=MAX_TOKEN_LEN) :: cmd%tokens(cmd%num_tokens - 1)) |
| 423 | cmd%tokens = temp_tokens |
| 424 | cmd%num_tokens = cmd%num_tokens - 1 |
| 425 | deallocate(temp_tokens) |
| 426 | else |
| 427 | ! Just "!" with no command - that's an error |
| 428 | write(error_unit, '(a)') '!: command not found' |
| 429 | shell%last_exit_status = 127 |
| 430 | return |
| 431 | end if |
| 432 | end if |
| 433 | |
| 434 | ! Capture command if we're inside a loop body (before executing control flow) |
| 435 | if (shell%control_depth > 0) then |
| 436 | if (shell%control_stack(shell%control_depth)%capturing_loop_body) then |
| 437 | if (allocated(cmd%tokens) .and. cmd%num_tokens > 0) then |
| 438 | ! Reconstruct the command from tokens (don't use original_input which may contain the whole line) |
| 439 | call reconstruct_command_from_tokens(cmd, reconstructed_cmd) |
| 440 | |
| 441 | ! Track nested depth for proper 'done' matching |
| 442 | if (trim(cmd%tokens(1)) == 'for' .or. trim(cmd%tokens(1)) == 'while' .or. & |
| 443 | (len_trim(cmd%tokens(1)) >= 5 .and. cmd%tokens(1)(1:5) == 'for((')) then |
| 444 | ! Starting a nested loop - increment nesting depth |
| 445 | shell%control_stack(shell%control_depth)%capture_nesting_depth = & |
| 446 | shell%control_stack(shell%control_depth)%capture_nesting_depth + 1 |
| 447 | call capture_loop_command(shell, trim(reconstructed_cmd)) |
| 448 | return |
| 449 | else if (trim(cmd%tokens(1)) == 'do') then |
| 450 | ! 'do' for nested loop - just capture it |
| 451 | if (shell%control_stack(shell%control_depth)%capture_nesting_depth > 0) then |
| 452 | call capture_loop_command(shell, trim(reconstructed_cmd)) |
| 453 | return |
| 454 | end if |
| 455 | ! If nesting depth is 0, this 'do' is an error - let it be processed |
| 456 | else if (trim(cmd%tokens(1)) == 'done') then |
| 457 | ! Check nesting depth |
| 458 | if (shell%control_stack(shell%control_depth)%capture_nesting_depth > 0) then |
| 459 | ! This 'done' ends a nested loop |
| 460 | shell%control_stack(shell%control_depth)%capture_nesting_depth = & |
| 461 | shell%control_stack(shell%control_depth)%capture_nesting_depth - 1 |
| 462 | call capture_loop_command(shell, trim(reconstructed_cmd)) |
| 463 | return |
| 464 | else |
| 465 | ! This 'done' ends the current capturing loop - process normally |
| 466 | end if |
| 467 | else |
| 468 | ! Everything else gets captured |
| 469 | call capture_loop_command(shell, trim(reconstructed_cmd)) |
| 470 | return |
| 471 | end if |
| 472 | end if |
| 473 | end if |
| 474 | end if |
| 475 | |
| 476 | ! Check for control flow keywords and apply control flow state |
| 477 | if (allocated(cmd%tokens) .and. cmd%num_tokens > 0) then |
| 478 | if (is_control_flow_keyword(cmd%tokens(1))) then |
| 479 | ! Special handling for single-line if statements: if condition; then command; fi |
| 480 | ! Check BEFORE processing control flow, because process_control_flow will set should_execute=false for "then"/"else" |
| 481 | if (trim(cmd%tokens(1)) == 'then' .and. cmd%num_tokens > 1) then |
| 482 | call process_control_flow(cmd, shell, should_execute) |
| 483 | call execute_inline_then_commands(cmd, shell) |
| 484 | return |
| 485 | end if |
| 486 | |
| 487 | ! Special handling for single-line else: else echo command |
| 488 | if (trim(cmd%tokens(1)) == 'else' .and. cmd%num_tokens > 1) then |
| 489 | call process_control_flow(cmd, shell, should_execute) |
| 490 | call execute_inline_then_commands(cmd, shell) ! Reuse same function since logic is identical |
| 491 | return |
| 492 | end if |
| 493 | |
| 494 | ! Special handling for single-line elif: elif condition; then echo command |
| 495 | if (trim(cmd%tokens(1)) == 'elif') then |
| 496 | call process_control_flow(cmd, shell, should_execute) |
| 497 | ! If the elif condition was true and we should execute, wait for the "then" keyword |
| 498 | ! The inline command will be handled when we see "then" |
| 499 | return |
| 500 | end if |
| 501 | |
| 502 | call process_control_flow(cmd, shell, should_execute) |
| 503 | |
| 504 | ! If we just processed a 'done' keyword, execute the loop body immediately |
| 505 | ! This ensures loops execute inline, not deferred until after the pipeline |
| 506 | if (trim(cmd%tokens(1)) == 'done') then |
| 507 | call replay_loop_if_needed(shell) |
| 508 | end if |
| 509 | |
| 510 | if (.not. should_execute) return |
| 511 | else |
| 512 | ! For regular commands, check if we should execute based on control flow state |
| 513 | call process_control_flow(cmd, shell, should_execute) |
| 514 | if (.not. should_execute) return |
| 515 | end if |
| 516 | end if |
| 517 | |
| 518 | ! Handle case pattern: skip first token if flag is set |
| 519 | if (shell%case_pattern_skip_first_token .and. cmd%num_tokens > 1) then |
| 520 | ! Shift tokens left to skip the pattern token |
| 521 | allocate(temp_tokens(cmd%num_tokens - 1)) |
| 522 | do i = 2, cmd%num_tokens |
| 523 | temp_tokens(i - 1) = cmd%tokens(i) |
| 524 | end do |
| 525 | deallocate(cmd%tokens) |
| 526 | allocate(character(len=MAX_TOKEN_LEN) :: cmd%tokens(cmd%num_tokens - 1)) |
| 527 | cmd%tokens = temp_tokens |
| 528 | cmd%num_tokens = cmd%num_tokens - 1 |
| 529 | deallocate(temp_tokens) |
| 530 | ! Reset the flag |
| 531 | shell%case_pattern_skip_first_token = .false. |
| 532 | end if |
| 533 | |
| 534 | ! Handle here document input |
| 535 | ! Only read from stdin if content wasn't already extracted from input string |
| 536 | if (allocated(cmd%heredoc_delimiter) .and. .not. allocated(cmd%heredoc_content)) then |
| 537 | call read_heredoc(cmd%heredoc_delimiter, cmd%heredoc_content, shell, cmd%heredoc_strip_tabs) |
| 538 | end if |
| 539 | |
| 540 | ! Check if this is a function definition BEFORE expanding variables |
| 541 | ! (so $1, $2, etc. remain literal in the function body) |
| 542 | if (is_function_definition_command(cmd, shell)) then |
| 543 | ! Function was registered, don't execute anything |
| 544 | shell%last_exit_status = 0 |
| 545 | return |
| 546 | end if |
| 547 | |
| 548 | ! Expand variables in all tokens (except for defun and assignments) |
| 549 | ! Skip expansion for assignments because execute_assignment needs to handle |
| 550 | ! quote-aware expansion properly (e.g., CMD="echo \$X" should store "echo $X") |
| 551 | ! Also skip if already pre-expanded in pipeline |
| 552 | if (.not. cmd%skip_expansion .and. & |
| 553 | trim(cmd%tokens(1)) /= 'defun' .and. & |
| 554 | .not. (index(cmd%tokens(1), '=') > 0 .and. index(cmd%tokens(1), '=') > 1)) then |
| 555 | ! Initialize token metadata if not set (for old parser path) |
| 556 | call init_token_metadata(cmd) |
| 557 | call expand_tokens(cmd, shell) |
| 558 | end if |
| 559 | |
| 560 | ! All tokens removed by expansion (e.g., unquoted $(exit 5) expands to nothing) |
| 561 | ! Preserve exit status from command substitution and return |
| 562 | if (cmd%num_tokens == 0) then |
| 563 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 564 | return |
| 565 | end if |
| 566 | |
| 567 | ! Check if parameter expansion error occurred (${VAR?error}) |
| 568 | if (shell%fatal_expansion_error) then |
| 569 | ! NOTE: Don't reset flag here - let it propagate to subshell handler |
| 570 | ! The subshell code uses this flag to convert exit code 127 to 1 (bash behavior) |
| 571 | ! End performance timing |
| 572 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 573 | ! POSIX: In non-interactive shells, exit the shell entirely |
| 574 | if (.not. shell%is_interactive) then |
| 575 | shell%running = .false. |
| 576 | end if |
| 577 | return ! Abort command execution |
| 578 | end if |
| 579 | |
| 580 | ! Check if arithmetic expansion error occurred |
| 581 | if (shell%arithmetic_error) then |
| 582 | shell%arithmetic_error = .false. ! Reset flag |
| 583 | ! End performance timing |
| 584 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 585 | return ! Abort command execution |
| 586 | end if |
| 587 | |
| 588 | ! Check for empty command after expansion (e.g., $(exit 0) returns empty) |
| 589 | if (cmd%num_tokens > 0 .and. len_trim(cmd%tokens(1)) == 0) then |
| 590 | ! Empty command after expansion - nothing to execute |
| 591 | ! Note: exit status from command substitution is preserved |
| 592 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 593 | return |
| 594 | end if |
| 595 | |
| 596 | ! Check for variable assignment (after expansion so ${...} is processed) |
| 597 | ! DISABLED: Now that we skip expand_tokens for assignments (line 503-506), |
| 598 | ! all assignments should go through execute_assignment which properly handles |
| 599 | ! backslash escapes in double-quoted strings. |
| 600 | ! if (cmd%num_tokens == 1 .and. is_assignment(cmd%tokens(1))) then |
| 601 | ! call handle_assignment(shell, cmd%tokens(1)) |
| 602 | ! return |
| 603 | ! end if |
| 604 | |
| 605 | ! Expand glob patterns (except for defun) |
| 606 | if (trim(cmd%tokens(1)) /= 'defun') then |
| 607 | call expand_command_globs(cmd, shell) |
| 608 | end if |
| 609 | |
| 610 | ! Process backslash escapes AFTER glob expansion |
| 611 | if (trim(cmd%tokens(1)) /= 'defun') then |
| 612 | call process_command_escapes(cmd) |
| 613 | end if |
| 614 | |
| 615 | ! === DEBUGGING & TRACING HOOKS === |
| 616 | ! Execute DEBUG trap if set (before command execution) |
| 617 | trap_executed = execute_trap(shell, TRAP_DEBUG) |
| 618 | |
| 619 | ! If a trap command was queued, execute it now |
| 620 | ! Check executing_trap to prevent recursion |
| 621 | ! Don't execute EXIT trap here - it should only execute when shell is exiting |
| 622 | if (len_trim(shell%pending_trap_command) > 0 .and. .not. shell%executing_trap .and. shell%pending_trap_signal /= 0) then |
| 623 | call execute_pending_trap(shell) |
| 624 | end if |
| 625 | |
| 626 | ! Trace command if xtrace is enabled (set -x) |
| 627 | if (shell%option_xtrace .and. cmd%num_tokens > 0) then |
| 628 | ! Reconstruct command for tracing |
| 629 | call reconstruct_command_from_tokens(cmd, reconstructed_cmd) |
| 630 | call trace_command(shell, trim(reconstructed_cmd)) |
| 631 | end if |
| 632 | ! === END DEBUGGING & TRACING HOOKS === |
| 633 | |
| 634 | ! Check for variable assignment: var=value or arr=(...) |
| 635 | ! Only recognize as assignment if name before = is valid (no $, `, etc.) |
| 636 | ! POSIX: Words containing $ before = are not assignments, they're commands |
| 637 | if (index(cmd%tokens(1), '=') > 0 .and. index(cmd%tokens(1), '=') > 1 .and. & |
| 638 | index(cmd%tokens(1)(1:index(cmd%tokens(1), '=')-1), '$') == 0) then |
| 639 | call execute_assignment(cmd, shell) |
| 640 | ! Check for ((expression)) arithmetic evaluation command |
| 641 | else if (len_trim(cmd%tokens(1)) >= 4 .and. & |
| 642 | cmd%tokens(1)(1:2) == '((' .and. & |
| 643 | cmd%tokens(1)(len_trim(cmd%tokens(1))-1:len_trim(cmd%tokens(1))) == '))') then |
| 644 | call execute_arithmetic_command(cmd, shell) |
| 645 | ! Check if it's a user-defined function (unless bypass_functions is set) |
| 646 | else if (.not. shell%bypass_functions .and. is_function(shell, cmd%tokens(1))) then |
| 647 | call execute_function(cmd, shell) |
| 648 | ! Eval is now handled as a regular builtin (no special case needed) |
| 649 | ! Check for cd-less navigation: if single token is a directory, treat as 'cd' |
| 650 | else if (cmd%num_tokens == 1 .and. file_is_directory(trim(cmd%tokens(1)))) then |
| 651 | ! Create synthetic cd command by properly reallocating tokens array |
| 652 | block |
| 653 | character(len=:), allocatable :: dir_path |
| 654 | integer :: token_len |
| 655 | ! Save the directory path |
| 656 | dir_path = trim(cmd%tokens(1)) |
| 657 | token_len = len(cmd%tokens) |
| 658 | ! Deallocate old tokens and allocate new array with size 2 |
| 659 | deallocate(cmd%tokens) |
| 660 | allocate(character(len=token_len) :: cmd%tokens(2)) |
| 661 | cmd%tokens(1) = 'cd' |
| 662 | cmd%tokens(2) = dir_path |
| 663 | cmd%num_tokens = 2 |
| 664 | end block |
| 665 | call execute_builtin_with_redirects(cmd, shell) |
| 666 | ! Check for alias expansion (unless bypass_aliases is set by 'command' builtin) |
| 667 | ! POSIX: Aliases are only expanded in interactive shells |
| 668 | else if (shell%is_interactive .and. .not. shell%bypass_aliases .and. is_alias(shell, cmd%tokens(1))) then |
| 669 | block |
| 670 | character(len=:), allocatable :: alias_command, expanded_command |
| 671 | character(len=4096) :: rest_of_command |
| 672 | integer :: j |
| 673 | type(pipeline_t) :: alias_pipeline |
| 674 | |
| 675 | ! Get the alias command |
| 676 | alias_command = get_alias(shell, cmd%tokens(1)) |
| 677 | |
| 678 | ! Build the rest of the command (arguments after the aliased command) |
| 679 | rest_of_command = '' |
| 680 | do j = 2, cmd%num_tokens |
| 681 | if (j > 2) rest_of_command = trim(rest_of_command) // ' ' |
| 682 | rest_of_command = trim(rest_of_command) // trim(cmd%tokens(j)) |
| 683 | end do |
| 684 | |
| 685 | ! Combine alias expansion with rest of arguments |
| 686 | if (len_trim(rest_of_command) > 0) then |
| 687 | expanded_command = trim(alias_command) // ' ' // trim(rest_of_command) |
| 688 | else |
| 689 | expanded_command = trim(alias_command) |
| 690 | end if |
| 691 | |
| 692 | ! Parse the expanded command |
| 693 | call parse_pipeline(expanded_command, alias_pipeline) |
| 694 | |
| 695 | if (alias_pipeline%num_commands > 0) then |
| 696 | ! Execute the expanded command |
| 697 | if (alias_pipeline%num_commands == 1) then |
| 698 | ! Single command - execute it directly with current redirections |
| 699 | ! Copy over any redirections from the original command |
| 700 | if (allocated(cmd%input_file)) then |
| 701 | alias_pipeline%commands(1)%input_file = cmd%input_file |
| 702 | end if |
| 703 | if (allocated(cmd%output_file)) then |
| 704 | alias_pipeline%commands(1)%output_file = cmd%output_file |
| 705 | alias_pipeline%commands(1)%append_output = cmd%append_output |
| 706 | alias_pipeline%commands(1)%force_clobber = cmd%force_clobber |
| 707 | end if |
| 708 | if (allocated(cmd%error_file)) then |
| 709 | alias_pipeline%commands(1)%error_file = cmd%error_file |
| 710 | alias_pipeline%commands(1)%append_error = cmd%append_error |
| 711 | end if |
| 712 | ! Copy other redirection flags |
| 713 | alias_pipeline%commands(1)%redirect_stderr_to_stdout = cmd%redirect_stderr_to_stdout |
| 714 | alias_pipeline%commands(1)%redirect_stdout_to_stderr = cmd%redirect_stdout_to_stderr |
| 715 | alias_pipeline%commands(1)%redirect_both_to_file = cmd%redirect_both_to_file |
| 716 | if (allocated(cmd%here_string)) then |
| 717 | alias_pipeline%commands(1)%here_string = cmd%here_string |
| 718 | end if |
| 719 | if (allocated(cmd%heredoc_delimiter)) then |
| 720 | alias_pipeline%commands(1)%heredoc_delimiter = cmd%heredoc_delimiter |
| 721 | alias_pipeline%commands(1)%heredoc_content = cmd%heredoc_content |
| 722 | alias_pipeline%commands(1)%heredoc_quoted = cmd%heredoc_quoted |
| 723 | end if |
| 724 | |
| 725 | ! Execute the single command recursively |
| 726 | call execute_single(alias_pipeline%commands(1), shell, expanded_command) |
| 727 | else |
| 728 | ! Multiple commands - execute as pipeline |
| 729 | call execute_pipeline(alias_pipeline, shell, expanded_command) |
| 730 | end if |
| 731 | |
| 732 | ! Clean up |
| 733 | do j = 1, alias_pipeline%num_commands |
| 734 | if (allocated(alias_pipeline%commands(j)%tokens)) deallocate(alias_pipeline%commands(j)%tokens) |
| 735 | if (allocated(alias_pipeline%commands(j)%input_file)) deallocate(alias_pipeline%commands(j)%input_file) |
| 736 | if (allocated(alias_pipeline%commands(j)%output_file)) deallocate(alias_pipeline%commands(j)%output_file) |
| 737 | if (allocated(alias_pipeline%commands(j)%error_file)) deallocate(alias_pipeline%commands(j)%error_file) |
| 738 | if (allocated(alias_pipeline%commands(j)%heredoc_delimiter)) deallocate(alias_pipeline%commands(j)%heredoc_delimiter) |
| 739 | if (allocated(alias_pipeline%commands(j)%heredoc_content)) deallocate(alias_pipeline%commands(j)%heredoc_content) |
| 740 | if (allocated(alias_pipeline%commands(j)%here_string)) deallocate(alias_pipeline%commands(j)%here_string) |
| 741 | if (allocated(alias_pipeline%commands(j)%group_content)) deallocate(alias_pipeline%commands(j)%group_content) |
| 742 | end do |
| 743 | if (allocated(alias_pipeline%commands)) deallocate(alias_pipeline%commands) |
| 744 | end if |
| 745 | end block |
| 746 | else if (is_builtin(cmd%tokens(1))) then |
| 747 | call execute_builtin_with_redirects(cmd, shell) |
| 748 | else |
| 749 | ! Check for command_not_found_handle before executing external |
| 750 | if (.not. shell%bypass_functions .and. & |
| 751 | is_function(shell, 'command_not_found_handle') .and. & |
| 752 | index(trim(cmd%tokens(1)), '/') == 0) then |
| 753 | block |
| 754 | logical :: cmd_found |
| 755 | integer :: ii |
| 756 | character(len=4096) :: path_var, candidate |
| 757 | character(len=:), allocatable :: path_dir |
| 758 | integer :: spos, cpos |
| 759 | |
| 760 | ! Inline PATH search to avoid command_builtin dependency |
| 761 | cmd_found = .false. |
| 762 | path_var = get_shell_variable(shell, 'PATH') |
| 763 | if (len_trim(path_var) == 0) path_var = '/usr/bin:/bin' |
| 764 | spos = 1 |
| 765 | do while (spos <= len_trim(path_var) .and. .not. cmd_found) |
| 766 | cpos = index(path_var(spos:), ':') |
| 767 | if (cpos == 0) then |
| 768 | path_dir = path_var(spos:len_trim(path_var)) |
| 769 | spos = len_trim(path_var) + 1 |
| 770 | else |
| 771 | path_dir = path_var(spos:spos + cpos - 2) |
| 772 | spos = spos + cpos |
| 773 | end if |
| 774 | if (len_trim(path_dir) == 0) path_dir = '.' |
| 775 | write(candidate, '(a,a,a)') trim(path_dir), '/', trim(cmd%tokens(1)) |
| 776 | cmd_found = file_exists(trim(candidate)) |
| 777 | end do |
| 778 | |
| 779 | if (.not. cmd_found) then |
| 780 | ! Build handler call string and dispatch via AST pipeline |
| 781 | ! (command_not_found_handle is an AST-cached function from eval) |
| 782 | block |
| 783 | use trap_dispatch, only: eval_trap_string |
| 784 | character(len=4096) :: handler_str |
| 785 | integer :: handler_exit |
| 786 | |
| 787 | handler_str = 'command_not_found_handle' |
| 788 | do ii = 1, cmd%num_tokens |
| 789 | handler_str = trim(handler_str) // ' ' // trim(cmd%tokens(ii)) |
| 790 | end do |
| 791 | call eval_trap_string(trim(handler_str), shell, handler_exit) |
| 792 | end block |
| 793 | else |
| 794 | call execute_external(cmd, shell, original_input) |
| 795 | end if |
| 796 | end block |
| 797 | else |
| 798 | call execute_external(cmd, shell, original_input) |
| 799 | end if |
| 800 | end if |
| 801 | |
| 802 | ! === ERROR TRAP HOOK === |
| 803 | ! Execute ERR trap if command failed (after command execution) |
| 804 | ! POSIX: ERR trap suppressed in same contexts as errexit: |
| 805 | ! - || / && lists, if/while/until conditions, negation (!) |
| 806 | if (shell%last_exit_status /= 0 .and. & |
| 807 | .not. shell%evaluating_condition .and. & |
| 808 | .not. shell%in_and_or_list .and. & |
| 809 | .not. shell%in_negation) then |
| 810 | trap_executed = execute_trap(shell, TRAP_ERR, shell%last_exit_status) |
| 811 | |
| 812 | ! If a trap command was queued, execute it now |
| 813 | ! Check executing_trap to prevent recursion |
| 814 | ! Don't execute EXIT trap here - it should only execute when shell is exiting |
| 815 | if (len_trim(shell%pending_trap_command) > 0 .and. .not. shell%executing_trap .and. shell%pending_trap_signal /= 0) then |
| 816 | call execute_pending_trap(shell) |
| 817 | end if |
| 818 | end if |
| 819 | ! === END ERROR TRAP HOOK === |
| 820 | |
| 821 | ! Handle exit status negation |
| 822 | if (negate_exit_status) then |
| 823 | if (shell%last_exit_status == 0) then |
| 824 | shell%last_exit_status = 1 |
| 825 | else |
| 826 | shell%last_exit_status = 0 |
| 827 | end if |
| 828 | end if |
| 829 | |
| 830 | ! End performance timing |
| 831 | call end_timer('execute_single', exec_start_time, total_exec_time) |
| 832 | end subroutine |
| 833 | |
| 834 | ! Execute builtin with redirection support |
| 835 | subroutine execute_builtin_with_redirects(cmd, shell) |
| 836 | type(command_t), intent(in) :: cmd |
| 837 | type(shell_state_t), intent(inout) :: shell |
| 838 | |
| 839 | integer :: saved_stdout, saved_stdin, saved_stderr |
| 840 | integer :: fd, ret, flags, i |
| 841 | integer, target :: pipefd(2) |
| 842 | integer(c_size_t) :: bytes_written |
| 843 | character(len=256), target :: c_filename |
| 844 | character(kind=c_char), target :: c_content(MAX_HEREDOC_LEN) |
| 845 | character(len=:), allocatable :: content_to_write, expanded_content |
| 846 | logical :: has_redirects, has_heredoc |
| 847 | ! Prefix assignment handling |
| 848 | character(len=MAX_TOKEN_LEN), allocatable :: saved_var_names(:), saved_var_values(:) |
| 849 | integer :: num_saved_vars, eq_pos, j |
| 850 | character(len=MAX_TOKEN_LEN) :: var_name, var_value |
| 851 | logical, allocatable :: var_was_set(:) |
| 852 | |
| 853 | ! Apply prefix assignments to shell variables (save old values first) |
| 854 | num_saved_vars = 0 |
| 855 | if (allocated(cmd%prefix_assignments)) then |
| 856 | allocate(saved_var_names(cmd%num_prefix_assignments)) |
| 857 | allocate(saved_var_values(cmd%num_prefix_assignments)) |
| 858 | allocate(var_was_set(cmd%num_prefix_assignments)) |
| 859 | do j = 1, cmd%num_prefix_assignments |
| 860 | eq_pos = index(cmd%prefix_assignments(j), '=') |
| 861 | if (eq_pos > 1) then |
| 862 | num_saved_vars = num_saved_vars + 1 |
| 863 | var_name = cmd%prefix_assignments(j)(:eq_pos-1) |
| 864 | var_value = cmd%prefix_assignments(j)(eq_pos+1:) |
| 865 | saved_var_names(num_saved_vars) = trim(var_name) |
| 866 | ! Save old value (empty string if not set) |
| 867 | saved_var_values(num_saved_vars) = get_shell_variable(shell, trim(var_name)) |
| 868 | var_was_set(num_saved_vars) = (len_trim(saved_var_values(num_saved_vars)) > 0) |
| 869 | ! Set new value |
| 870 | call var_set_shell_variable(shell, trim(var_name), trim(var_value)) |
| 871 | end if |
| 872 | end do |
| 873 | end if ! allocated(prefix_assignments) |
| 874 | |
| 875 | ! Check if we have any redirections |
| 876 | has_redirects = allocated(cmd%output_file) .or. allocated(cmd%input_file) .or. & |
| 877 | allocated(cmd%error_file) .or. cmd%redirect_stderr_to_stdout .or. & |
| 878 | cmd%redirect_stdout_to_stderr |
| 879 | has_heredoc = allocated(cmd%heredoc_content) .or. allocated(cmd%here_string) |
| 880 | |
| 881 | if (.not. has_redirects .and. .not. has_heredoc) then |
| 882 | ! No redirections, just execute the builtin normally |
| 883 | call execute_builtin(cmd, shell) |
| 884 | ! Restore prefix assignment variables |
| 885 | do j = 1, num_saved_vars |
| 886 | if (var_was_set(j)) then |
| 887 | call var_set_shell_variable(shell, trim(saved_var_names(j)), trim(saved_var_values(j))) |
| 888 | else |
| 889 | ! Variable wasn't set before, we should unset it |
| 890 | ! For now, just set to empty (proper unset would need more work) |
| 891 | call var_set_shell_variable(shell, trim(saved_var_names(j)), '') |
| 892 | end if |
| 893 | end do |
| 894 | return |
| 895 | end if |
| 896 | |
| 897 | ! Save current file descriptors |
| 898 | saved_stdout = c_dup(STDOUT_FD) |
| 899 | saved_stdin = c_dup(STDIN_FD) |
| 900 | saved_stderr = c_dup(STDERR_FD) |
| 901 | |
| 902 | ! Handle heredoc/here-string input (redirects stdin) |
| 903 | if (has_heredoc) then |
| 904 | ! Prepare content to write |
| 905 | if (allocated(cmd%here_string)) then |
| 906 | content_to_write = cmd%here_string // char(10) ! Add newline |
| 907 | else if (allocated(cmd%heredoc_content)) then |
| 908 | ! Check if we should expand variables (unquoted delimiter) |
| 909 | if (.not. cmd%heredoc_quoted) then |
| 910 | ! Expand variables in heredoc content |
| 911 | call expand_variables(cmd%heredoc_content, expanded_content, shell) |
| 912 | if (allocated(expanded_content)) then |
| 913 | content_to_write = expanded_content |
| 914 | else |
| 915 | content_to_write = cmd%heredoc_content |
| 916 | end if |
| 917 | else |
| 918 | ! Quoted delimiter - use content as-is |
| 919 | content_to_write = cmd%heredoc_content |
| 920 | end if |
| 921 | end if |
| 922 | |
| 923 | ! Create pipe and redirect stdin |
| 924 | if (allocated(content_to_write)) then |
| 925 | ret = c_pipe(c_loc(pipefd)) |
| 926 | if (ret == 0) then |
| 927 | ! Convert content to C string |
| 928 | do i = 1, min(len(content_to_write), MAX_HEREDOC_LEN-1) |
| 929 | c_content(i) = content_to_write(i:i) |
| 930 | end do |
| 931 | c_content(min(len(content_to_write), MAX_HEREDOC_LEN-1)+1) = c_null_char |
| 932 | |
| 933 | ! Write content to pipe |
| 934 | bytes_written = c_write(pipefd(2), c_loc(c_content), & |
| 935 | int(min(len(content_to_write), MAX_HEREDOC_LEN-1), c_size_t)) |
| 936 | |
| 937 | ! Close write end and redirect stdin to read end |
| 938 | ret = c_close(pipefd(2)) |
| 939 | ret = c_dup2(pipefd(1), STDIN_FD) |
| 940 | ret = c_close(pipefd(1)) |
| 941 | end if |
| 942 | end if |
| 943 | end if |
| 944 | |
| 945 | ! Handle input redirection (file) |
| 946 | if (allocated(cmd%input_file)) then |
| 947 | c_filename = trim(cmd%input_file)//c_null_char |
| 948 | fd = c_open(c_loc(c_filename), O_RDONLY, 0) |
| 949 | if (fd >= 0) then |
| 950 | ret = c_dup2(fd, STDIN_FD) |
| 951 | ret = c_close(fd) |
| 952 | else |
| 953 | write(error_unit, '(3a)') 'fortsh: cannot open input file: ', trim(cmd%input_file) |
| 954 | shell%last_exit_status = 1 |
| 955 | ! Restore prefix assignment variables before returning |
| 956 | do j = 1, num_saved_vars |
| 957 | if (var_was_set(j)) then |
| 958 | call var_set_shell_variable(shell, trim(saved_var_names(j)), trim(saved_var_values(j))) |
| 959 | else |
| 960 | call var_set_shell_variable(shell, trim(saved_var_names(j)), '') |
| 961 | end if |
| 962 | end do |
| 963 | return |
| 964 | end if |
| 965 | end if |
| 966 | |
| 967 | ! Handle output redirection |
| 968 | if (allocated(cmd%output_file)) then |
| 969 | ! Check noclobber protection |
| 970 | if (shell%option_noclobber .and. .not. cmd%force_clobber .and. .not. cmd%append_output) then |
| 971 | ! Check if file exists |
| 972 | if (file_exists(trim(cmd%output_file))) then |
| 973 | write(error_unit, '(3a)') 'fortsh: ', trim(cmd%output_file), ': cannot overwrite existing file' |
| 974 | shell%last_exit_status = 1 |
| 975 | ! Restore stdin before returning |
| 976 | ret = c_dup2(saved_stdin, STDIN_FD) |
| 977 | ret = c_close(saved_stdin) |
| 978 | ! Restore prefix assignment variables before returning |
| 979 | do j = 1, num_saved_vars |
| 980 | if (var_was_set(j)) then |
| 981 | call var_set_shell_variable(shell, trim(saved_var_names(j)), trim(saved_var_values(j))) |
| 982 | else |
| 983 | call var_set_shell_variable(shell, trim(saved_var_names(j)), '') |
| 984 | end if |
| 985 | end do |
| 986 | return |
| 987 | end if |
| 988 | end if |
| 989 | |
| 990 | if (cmd%append_output) then |
| 991 | flags = ior(ior(O_WRONLY, O_CREAT), O_APPEND) |
| 992 | else |
| 993 | flags = ior(ior(O_WRONLY, O_CREAT), O_TRUNC) |
| 994 | end if |
| 995 | |
| 996 | c_filename = trim(cmd%output_file)//c_null_char |
| 997 | fd = c_open(c_loc(c_filename), flags, int(o'644', c_int)) |
| 998 | if (fd >= 0) then |
| 999 | ret = c_dup2(fd, STDOUT_FD) |
| 1000 | ret = c_close(fd) |
| 1001 | else |
| 1002 | write(error_unit, '(3a)') 'fortsh: cannot open output file: ', trim(cmd%output_file) |
| 1003 | shell%last_exit_status = 1 |
| 1004 | ! Restore stdin before returning |
| 1005 | ret = c_dup2(saved_stdin, STDIN_FD) |
| 1006 | ret = c_close(saved_stdin) |
| 1007 | ! Restore prefix assignment variables before returning |
| 1008 | do j = 1, num_saved_vars |
| 1009 | if (var_was_set(j)) then |
| 1010 | call var_set_shell_variable(shell, trim(saved_var_names(j)), trim(saved_var_values(j))) |
| 1011 | else |
| 1012 | call var_set_shell_variable(shell, trim(saved_var_names(j)), '') |
| 1013 | end if |
| 1014 | end do |
| 1015 | return |
| 1016 | end if |
| 1017 | end if |
| 1018 | |
| 1019 | ! Handle error redirection |
| 1020 | if (allocated(cmd%error_file)) then |
| 1021 | if (cmd%append_error) then |
| 1022 | flags = ior(ior(O_WRONLY, O_CREAT), O_APPEND) |
| 1023 | else |
| 1024 | flags = ior(ior(O_WRONLY, O_CREAT), O_TRUNC) |
| 1025 | end if |
| 1026 | |
| 1027 | c_filename = trim(cmd%error_file)//c_null_char |
| 1028 | fd = c_open(c_loc(c_filename), flags, int(o'644', c_int)) |
| 1029 | if (fd >= 0) then |
| 1030 | ret = c_dup2(fd, STDERR_FD) |
| 1031 | ret = c_close(fd) |
| 1032 | else |
| 1033 | write(error_unit, '(3a)') 'fortsh: cannot open error file: ', trim(cmd%error_file) |
| 1034 | shell%last_exit_status = 1 |
| 1035 | ! Restore fds before returning |
| 1036 | ret = c_dup2(saved_stdin, STDIN_FD) |
| 1037 | ret = c_dup2(saved_stdout, STDOUT_FD) |
| 1038 | ret = c_close(saved_stdin) |
| 1039 | ret = c_close(saved_stdout) |
| 1040 | ! Restore prefix assignment variables before returning |
| 1041 | do j = 1, num_saved_vars |
| 1042 | if (var_was_set(j)) then |
| 1043 | call var_set_shell_variable(shell, trim(saved_var_names(j)), trim(saved_var_values(j))) |
| 1044 | else |
| 1045 | call var_set_shell_variable(shell, trim(saved_var_names(j)), '') |
| 1046 | end if |
| 1047 | end do |
| 1048 | return |
| 1049 | end if |
| 1050 | end if |
| 1051 | |
| 1052 | ! Handle advanced redirections |
| 1053 | if (cmd%redirect_stderr_to_stdout) then |
| 1054 | ret = c_dup2(STDOUT_FD, STDERR_FD) |
| 1055 | end if |
| 1056 | |
| 1057 | if (cmd%redirect_stdout_to_stderr) then |
| 1058 | ret = c_dup2(STDERR_FD, STDOUT_FD) |
| 1059 | end if |
| 1060 | |
| 1061 | ! Execute the builtin |
| 1062 | call execute_builtin(cmd, shell) |
| 1063 | |
| 1064 | ! Restore original file descriptors |
| 1065 | ret = c_dup2(saved_stdout, STDOUT_FD) |
| 1066 | ret = c_dup2(saved_stdin, STDIN_FD) |
| 1067 | ret = c_dup2(saved_stderr, STDERR_FD) |
| 1068 | ret = c_close(saved_stdout) |
| 1069 | ret = c_close(saved_stdin) |
| 1070 | ret = c_close(saved_stderr) |
| 1071 | |
| 1072 | ! Restore prefix assignment variables |
| 1073 | do j = 1, num_saved_vars |
| 1074 | if (var_was_set(j)) then |
| 1075 | call var_set_shell_variable(shell, trim(saved_var_names(j)), trim(saved_var_values(j))) |
| 1076 | else |
| 1077 | ! Variable wasn't set before, set to empty |
| 1078 | call var_set_shell_variable(shell, trim(saved_var_names(j)), '') |
| 1079 | end if |
| 1080 | end do |
| 1081 | end subroutine |
| 1082 | |
| 1083 | ! Execute ((expression)) arithmetic evaluation command |
| 1084 | ! Sets exit status to 0 if expression is non-zero, 1 if zero |
| 1085 | subroutine execute_arithmetic_command(cmd, shell) |
| 1086 | use expansion, only: arithmetic_expansion_shell |
| 1087 | type(command_t), intent(in) :: cmd |
| 1088 | type(shell_state_t), intent(inout) :: shell |
| 1089 | character(len=:), allocatable :: expr, result_str |
| 1090 | character(len=:), allocatable :: arith_expr |
| 1091 | integer(kind=8) :: result_val |
| 1092 | integer :: iostat |
| 1093 | |
| 1094 | ! Build full expression from all tokens |
| 1095 | expr = trim(cmd%tokens(1)) |
| 1096 | |
| 1097 | ! Convert ((expr)) to $((expr)) for arithmetic_expansion_shell |
| 1098 | arith_expr = '$' // trim(expr) |
| 1099 | |
| 1100 | ! Evaluate arithmetic expression |
| 1101 | result_str = arithmetic_expansion_shell(trim(arith_expr), shell) |
| 1102 | |
| 1103 | ! Check if there was an error (empty result indicates error) |
| 1104 | if (len_trim(result_str) == 0) then |
| 1105 | ! There was an arithmetic error, exit status already set by arithmetic_expansion_shell |
| 1106 | ! Just return without changing it |
| 1107 | return |
| 1108 | end if |
| 1109 | |
| 1110 | ! Convert result to integer |
| 1111 | read(result_str, *, iostat=iostat) result_val |
| 1112 | if (iostat /= 0) result_val = 0 |
| 1113 | |
| 1114 | ! Set exit status: 0 if non-zero, 1 if zero |
| 1115 | if (result_val /= 0) then |
| 1116 | shell%last_exit_status = 0 |
| 1117 | else |
| 1118 | shell%last_exit_status = 1 |
| 1119 | end if |
| 1120 | end subroutine |
| 1121 | |
| 1122 | ! Execute variable assignment: var=value, arr=(a b c), or arr[0]=value |
| 1123 | subroutine execute_assignment(cmd, shell) |
| 1124 | type(command_t), intent(in) :: cmd |
| 1125 | type(shell_state_t), intent(inout) :: shell |
| 1126 | character(len=MAX_TOKEN_LEN) :: var_name, var_value, token |
| 1127 | ! Heap-allocated to avoid static storage in recursive context |
| 1128 | character(len=MAX_TOKEN_LEN), allocatable :: array_elements(:) |
| 1129 | character(len=100) :: index_str |
| 1130 | character(len=:), allocatable :: expanded_value |
| 1131 | integer :: eq_pos, paren_start, paren_end, num_elements, bracket_pos |
| 1132 | integer :: bracket_end, array_index, read_status, actual_value_len, i, token_len |
| 1133 | logical :: is_indexed_assignment |
| 1134 | character(len=1) :: quote_char_temp |
| 1135 | |
| 1136 | allocate(array_elements(30)) |
| 1137 | |
| 1138 | ! For quoted tokens, preserve whitespace by not trimming |
| 1139 | ! For unquoted tokens, trim is safe |
| 1140 | ! NOTE: Check allocation first to avoid accessing unallocated arrays |
| 1141 | if (allocated(cmd%token_quoted) .and. allocated(cmd%token_lengths)) then |
| 1142 | if (size(cmd%token_quoted) >= 1 .and. cmd%token_quoted(1)) then |
| 1143 | ! Quoted token - preserve whitespace, track actual length |
| 1144 | ! Use token_lengths array if available, otherwise fall back to len() |
| 1145 | if (size(cmd%token_lengths) >= 1) then |
| 1146 | token_len = cmd%token_lengths(1) |
| 1147 | else |
| 1148 | token_len = len(cmd%tokens(1)) |
| 1149 | end if |
| 1150 | token = cmd%tokens(1) |
| 1151 | else |
| 1152 | ! Token arrays allocated but this token not quoted - use standard processing |
| 1153 | token = cmd%tokens(1) |
| 1154 | token_len = len_trim(token) |
| 1155 | end if |
| 1156 | else |
| 1157 | ! Old parser: token may contain quotes with whitespace inside |
| 1158 | ! We need to find the actual token end, not use len_trim which strips whitespace |
| 1159 | token = cmd%tokens(1) |
| 1160 | |
| 1161 | ! Find the = sign to check if value is quoted |
| 1162 | eq_pos = index(token, '=') |
| 1163 | if (eq_pos > 0 .and. eq_pos < len(token)) then |
| 1164 | ! Check if value starts with a quote |
| 1165 | if (token(eq_pos+1:eq_pos+1) == '"' .or. token(eq_pos+1:eq_pos+1) == "'") then |
| 1166 | quote_char_temp = token(eq_pos+1:eq_pos+1) |
| 1167 | ! Find the closing quote to determine actual length |
| 1168 | do i = eq_pos + 2, len(token) |
| 1169 | if (token(i:i) == quote_char_temp) then |
| 1170 | token_len = i |
| 1171 | exit |
| 1172 | end if |
| 1173 | end do |
| 1174 | ! If no closing quote found, fall back to len_trim |
| 1175 | if (i > len(token)) then |
| 1176 | token_len = len_trim(token) |
| 1177 | end if |
| 1178 | else |
| 1179 | ! Unquoted value - trim is safe |
| 1180 | token_len = len_trim(token) |
| 1181 | end if |
| 1182 | else |
| 1183 | token_len = len_trim(token) |
| 1184 | end if |
| 1185 | end if |
| 1186 | eq_pos = index(token, '=') |
| 1187 | if (eq_pos == 0) return |
| 1188 | |
| 1189 | ! Check for array index assignment: arr[index]=value |
| 1190 | bracket_pos = index(token, '[') |
| 1191 | is_indexed_assignment = (bracket_pos > 0 .and. bracket_pos < eq_pos) |
| 1192 | |
| 1193 | if (is_indexed_assignment) then |
| 1194 | ! arr[index]=value |
| 1195 | var_name = token(:bracket_pos-1) |
| 1196 | bracket_end = index(token(bracket_pos:token_len), ']') |
| 1197 | if (bracket_end > 0) then |
| 1198 | bracket_end = bracket_pos + bracket_end - 1 |
| 1199 | index_str = token(bracket_pos+1:bracket_end-1) |
| 1200 | var_value = token(eq_pos+1:token_len) |
| 1201 | |
| 1202 | ! Strip quotes and lexer sentinel chars from array subscript |
| 1203 | block |
| 1204 | use variables, only: strip_quotes |
| 1205 | character(len=100) :: clean_key |
| 1206 | integer :: ci, co |
| 1207 | call strip_quotes(index_str) |
| 1208 | ! Remove sentinel chars (char(1), char(2), char(3)) |
| 1209 | co = 0 |
| 1210 | clean_key = '' |
| 1211 | do ci = 1, len_trim(index_str) |
| 1212 | if (ichar(index_str(ci:ci)) > 3) then |
| 1213 | co = co + 1 |
| 1214 | clean_key(co:co) = index_str(ci:ci) |
| 1215 | end if |
| 1216 | end do |
| 1217 | index_str = clean_key |
| 1218 | end block |
| 1219 | |
| 1220 | ! Expand variables/command substitutions in value |
| 1221 | if (index(var_value, '$') > 0 .or. index(var_value, '~') > 0) then |
| 1222 | block |
| 1223 | use parser, only: expand_variables |
| 1224 | call expand_variables(var_value, expanded_value, shell) |
| 1225 | if (allocated(expanded_value)) then |
| 1226 | var_value = expanded_value |
| 1227 | end if |
| 1228 | end block |
| 1229 | end if |
| 1230 | |
| 1231 | ! Check associative array first (numeric keys are valid) |
| 1232 | block |
| 1233 | use variables, only: is_associative_array, set_assoc_array_value |
| 1234 | if (is_associative_array(shell, trim(var_name))) then |
| 1235 | call set_assoc_array_value(shell, trim(var_name), & |
| 1236 | trim(index_str), trim(var_value)) |
| 1237 | shell%last_exit_status = 0 |
| 1238 | else |
| 1239 | ! Parse as numeric index (0-indexed → 1-indexed) |
| 1240 | read(index_str, *, iostat=read_status) array_index |
| 1241 | if (read_status == 0) then |
| 1242 | array_index = array_index + 1 |
| 1243 | call set_array_element(shell, trim(var_name), & |
| 1244 | array_index, trim(var_value)) |
| 1245 | shell%last_exit_status = 0 |
| 1246 | else |
| 1247 | write(error_unit, '(a)') & |
| 1248 | 'Error: invalid array index' |
| 1249 | shell%last_exit_status = 1 |
| 1250 | end if |
| 1251 | end if |
| 1252 | end block |
| 1253 | else |
| 1254 | write(error_unit, '(a)') 'Error: unclosed bracket in array assignment' |
| 1255 | shell%last_exit_status = 1 |
| 1256 | end if |
| 1257 | return |
| 1258 | end if |
| 1259 | |
| 1260 | ! Get variable name (before =) |
| 1261 | var_name = token(:eq_pos-1) |
| 1262 | |
| 1263 | ! Check for += append syntax: arr+=(...) |
| 1264 | block |
| 1265 | logical :: is_append |
| 1266 | is_append = .false. |
| 1267 | if (eq_pos >= 2 .and. token(eq_pos-1:eq_pos-1) == '+') then |
| 1268 | is_append = .true. |
| 1269 | var_name = token(:eq_pos-2) |
| 1270 | end if |
| 1271 | |
| 1272 | ! Check if it's an array literal: arr=(...) or arr+=(...) |
| 1273 | paren_start = eq_pos + 1 |
| 1274 | if (paren_start <= token_len .and. token(paren_start:paren_start) == '(') then |
| 1275 | ! Array literal |
| 1276 | paren_end = index(token(paren_start+1:token_len), ')') |
| 1277 | if (paren_end > 0) then |
| 1278 | paren_end = paren_start + paren_end |
| 1279 | ! Extract elements between parentheses |
| 1280 | var_value = token(paren_start+1:paren_end-1) |
| 1281 | |
| 1282 | ! Expand command substitutions and variables |
| 1283 | if (index(trim(var_value), '$') > 0 .or. & |
| 1284 | index(trim(var_value), '`') > 0) then |
| 1285 | block |
| 1286 | use parser, only: expand_variables |
| 1287 | character(len=:), allocatable :: arr_exp |
| 1288 | call expand_variables(trim(var_value), & |
| 1289 | arr_exp, shell) |
| 1290 | if (allocated(arr_exp)) then |
| 1291 | var_value = arr_exp |
| 1292 | end if |
| 1293 | end block |
| 1294 | end if |
| 1295 | |
| 1296 | ! Split by spaces to get array elements |
| 1297 | num_elements = 0 |
| 1298 | call split_array_elements(var_value, array_elements, num_elements) |
| 1299 | |
| 1300 | if (is_append) then |
| 1301 | ! Append to existing array |
| 1302 | block |
| 1303 | integer :: existing_size, k |
| 1304 | existing_size = get_array_size(shell, trim(var_name)) |
| 1305 | do k = 1, num_elements |
| 1306 | call set_array_element(shell, trim(var_name), & |
| 1307 | existing_size + k, trim(array_elements(k))) |
| 1308 | end do |
| 1309 | end block |
| 1310 | else |
| 1311 | ! Set as array variable |
| 1312 | call set_array_variable(shell, trim(var_name), & |
| 1313 | array_elements, num_elements) |
| 1314 | end if |
| 1315 | shell%last_exit_status = 0 |
| 1316 | else |
| 1317 | write(error_unit, '(a)') 'Error: unclosed array literal' |
| 1318 | shell%last_exit_status = 1 |
| 1319 | end if |
| 1320 | else |
| 1321 | ! Simple assignment: var=value |
| 1322 | ! Use token_len to avoid including padding for quoted tokens |
| 1323 | var_value = token(eq_pos+1:token_len) |
| 1324 | |
| 1325 | ! Expand variables in the value (including parameter expansions like ${var##pattern}) |
| 1326 | ! IMPORTANT: Call expand_variables BEFORE stripping quotes, so it can apply |
| 1327 | ! correct backslash escape handling for double-quoted strings |
| 1328 | ! expand_variables will strip outer quotes automatically |
| 1329 | if (index(var_value, '$') > 0 .or. index(var_value, '~') > 0) then |
| 1330 | call expand_variables(var_value, expanded_value, shell) |
| 1331 | |
| 1332 | ! Check if arithmetic expansion error occurred |
| 1333 | if (shell%arithmetic_error) then |
| 1334 | shell%arithmetic_error = .false. ! Reset flag |
| 1335 | shell%last_exit_status = 127 ! POSIX sh returns 127 for arithmetic errors |
| 1336 | return ! Abort assignment |
| 1337 | end if |
| 1338 | |
| 1339 | ! POSIX: Exit status of assignment with command substitution is from last substitution |
| 1340 | ! Don't overwrite the exit status that was set by execute_command_and_capture |
| 1341 | |
| 1342 | if (allocated(expanded_value)) then |
| 1343 | ! For expanded values, use the allocated length |
| 1344 | call var_set_shell_variable(shell, trim(var_name), expanded_value, len(expanded_value)) |
| 1345 | else |
| 1346 | call var_set_shell_variable(shell, trim(var_name), '', 0) |
| 1347 | end if |
| 1348 | else |
| 1349 | ! No variable expansion needed |
| 1350 | ! Calculate actual length from token positions (NOT len_trim, to preserve whitespace) |
| 1351 | actual_value_len = token_len - eq_pos |
| 1352 | |
| 1353 | ! Strip outer quotes if present (old parser keeps quotes in tokens) |
| 1354 | if (actual_value_len >= 2) then |
| 1355 | if ((var_value(1:1) == '"' .and. & |
| 1356 | var_value(actual_value_len:actual_value_len) == '"') & |
| 1357 | .or. (var_value(1:1) == "'" .and. & |
| 1358 | var_value(actual_value_len:actual_value_len) == "'")) & |
| 1359 | then |
| 1360 | ! Remove quotes and adjust length |
| 1361 | var_value = var_value(2:actual_value_len-1) |
| 1362 | actual_value_len = actual_value_len - 2 |
| 1363 | end if |
| 1364 | end if |
| 1365 | |
| 1366 | ! Check for integer attribute: evaluate as arithmetic |
| 1367 | block |
| 1368 | logical :: is_int_var |
| 1369 | is_int_var = .false. |
| 1370 | do i = 1, shell%num_variables |
| 1371 | if (trim(shell%variables(i)%name) == & |
| 1372 | trim(var_name)) then |
| 1373 | is_int_var = shell%variables(i)%is_integer |
| 1374 | exit |
| 1375 | end if |
| 1376 | end do |
| 1377 | if (is_int_var .and. actual_value_len > 0) then |
| 1378 | block |
| 1379 | use expansion, only: arithmetic_expansion_shell |
| 1380 | character(len=:), allocatable :: arith_expr, arith_result |
| 1381 | arith_expr = '$((' // & |
| 1382 | var_value(:actual_value_len) // '))' |
| 1383 | arith_result = & |
| 1384 | arithmetic_expansion_shell( & |
| 1385 | trim(arith_expr), shell) |
| 1386 | var_value = arith_result |
| 1387 | actual_value_len = len_trim(var_value) |
| 1388 | end block |
| 1389 | end if |
| 1390 | end block |
| 1391 | |
| 1392 | call var_set_shell_variable(shell, trim(var_name), & |
| 1393 | var_value, actual_value_len) |
| 1394 | ! Set exit status to 0 for simple assignments without expansions |
| 1395 | ! But don't overwrite error status from readonly violation |
| 1396 | if (shell%last_exit_status /= 127) then |
| 1397 | shell%last_exit_status = 0 |
| 1398 | end if |
| 1399 | end if |
| 1400 | |
| 1401 | ! If allexport is enabled (set -a), automatically export the variable |
| 1402 | if (shell%option_allexport) then |
| 1403 | do i = 1, shell%num_variables |
| 1404 | if (trim(shell%variables(i)%name) == trim(var_name)) then |
| 1405 | shell%variables(i)%exported = .true. |
| 1406 | ! Also set in environment |
| 1407 | if (.not. set_environment_var(trim(var_name), trim(shell%variables(i)%value))) then |
| 1408 | ! Silently ignore export errors (POSIX behavior) |
| 1409 | end if |
| 1410 | exit |
| 1411 | end if |
| 1412 | end do |
| 1413 | end if |
| 1414 | |
| 1415 | ! POSIX: Exit status of assignment is from last command substitution |
| 1416 | ! Only set to 0 if no expansion was performed (i.e., no command substitution) |
| 1417 | ! Don't overwrite exit status when there was a command substitution |
| 1418 | end if |
| 1419 | end block |
| 1420 | end subroutine |
| 1421 | |
| 1422 | ! Split array elements by spaces (respecting quotes) |
| 1423 | subroutine split_array_elements(input, elements, count) |
| 1424 | character(len=*), intent(in) :: input |
| 1425 | character(len=MAX_TOKEN_LEN), intent(out) :: elements(:) |
| 1426 | integer, intent(out) :: count |
| 1427 | integer :: i, start, len_input |
| 1428 | logical :: in_quotes |
| 1429 | character :: quote_char |
| 1430 | |
| 1431 | count = 0 |
| 1432 | i = 1 |
| 1433 | len_input = len_trim(input) |
| 1434 | |
| 1435 | do while (i <= len_input) |
| 1436 | ! Skip leading spaces |
| 1437 | do while (i <= len_input .and. input(i:i) == ' ') |
| 1438 | i = i + 1 |
| 1439 | end do |
| 1440 | if (i > len_input) exit |
| 1441 | |
| 1442 | ! Start of element |
| 1443 | count = count + 1 |
| 1444 | if (count > size(elements)) exit |
| 1445 | start = i |
| 1446 | in_quotes = .false. |
| 1447 | quote_char = ' ' |
| 1448 | |
| 1449 | ! Find end of element |
| 1450 | do while (i <= len_input) |
| 1451 | if (.not. in_quotes .and. (input(i:i) == '"' .or. input(i:i) == "'")) then |
| 1452 | in_quotes = .true. |
| 1453 | quote_char = input(i:i) |
| 1454 | else if (in_quotes .and. input(i:i) == quote_char) then |
| 1455 | in_quotes = .false. |
| 1456 | else if (.not. in_quotes .and. input(i:i) == ' ') then |
| 1457 | exit |
| 1458 | end if |
| 1459 | i = i + 1 |
| 1460 | end do |
| 1461 | |
| 1462 | ! Extract element (and remove quotes if present) |
| 1463 | elements(count) = input(start:i-1) |
| 1464 | if (len_trim(elements(count)) >= 2) then |
| 1465 | if ((elements(count)(1:1) == '"' .and. elements(count)(len_trim(elements(count)):len_trim(elements(count))) == '"') .or. & |
| 1466 | (elements(count)(1:1) == "'" .and. elements(count)(len_trim(elements(count)):len_trim(elements(count))) == "'")) then |
| 1467 | elements(count) = elements(count)(2:len_trim(elements(count))-1) |
| 1468 | end if |
| 1469 | end if |
| 1470 | end do |
| 1471 | end subroutine |
| 1472 | |
| 1473 | subroutine execute_external(cmd, shell, original_input) |
| 1474 | type(command_t), intent(in) :: cmd |
| 1475 | type(shell_state_t), intent(inout) :: shell |
| 1476 | character(len=*), intent(in) :: original_input |
| 1477 | |
| 1478 | integer(c_pid_t) :: pid, pgid, ret |
| 1479 | integer(c_int), target :: wait_status |
| 1480 | integer(c_int) :: pgid_ret |
| 1481 | integer :: job_id |
| 1482 | logical :: foreground |
| 1483 | type(c_funptr) :: old_handler |
| 1484 | |
| 1485 | foreground = .not. cmd%background |
| 1486 | |
| 1487 | ! Check for empty command before forking (e.g., from empty command substitution) |
| 1488 | ! This preserves the exit status from the command substitution |
| 1489 | if (cmd%num_tokens < 1 .or. len_trim(cmd%tokens(1)) == 0) then |
| 1490 | ! Empty command - nothing to execute, preserve current exit status |
| 1491 | return |
| 1492 | end if |
| 1493 | |
| 1494 | ! CRITICAL: Re-ensure SIGCHLD is SIG_DFL before forking |
| 1495 | ! Something in interactive mode might be resetting it |
| 1496 | pid = c_fork() |
| 1497 | |
| 1498 | if (pid < 0) then |
| 1499 | write(error_unit, '(a)') 'Error: fork failed' |
| 1500 | shell%last_exit_status = 1 |
| 1501 | else if (pid == 0) then |
| 1502 | ! Child process |
| 1503 | if (.not. shell%in_pipeline_child) then |
| 1504 | ! Only set up own process group when NOT in a pipeline child. |
| 1505 | ! Pipeline children inherit their process group from the AST |
| 1506 | ! pipeline executor which manages groups at the pipeline level. |
| 1507 | pgid = c_getpid() |
| 1508 | ret = c_setpgid(0, pgid) |
| 1509 | end if |
| 1510 | |
| 1511 | ! Reset signal handlers to default |
| 1512 | old_handler = c_signal(SIGINT, c_null_funptr) |
| 1513 | old_handler = c_signal(SIGPIPE, c_null_funptr) |
| 1514 | old_handler = c_signal(SIGTSTP, c_null_funptr) |
| 1515 | old_handler = c_signal(SIGTTIN, c_null_funptr) |
| 1516 | old_handler = c_signal(SIGTTOU, c_null_funptr) |
| 1517 | |
| 1518 | ! Handle here document |
| 1519 | call handle_heredoc(cmd, shell) |
| 1520 | |
| 1521 | ! Apply prefix assignments to environment (VAR=value command) |
| 1522 | call apply_prefix_assignments(cmd) |
| 1523 | |
| 1524 | ! Set up redirections |
| 1525 | call setup_redirections(cmd, shell) |
| 1526 | |
| 1527 | ! Execute |
| 1528 | call exec_child(cmd%tokens, cmd%num_tokens) |
| 1529 | ! Error message is printed by exec_child if command not found |
| 1530 | call c_exit(127) |
| 1531 | else |
| 1532 | ! Parent process |
| 1533 | shell%last_pid = pid |
| 1534 | |
| 1535 | ! Auto-populate hash table (hashall) |
| 1536 | if (shell%option_hashall .and. & |
| 1537 | index(trim(cmd%tokens(1)), '/') == 0) then |
| 1538 | call cache_command_path(shell, trim(cmd%tokens(1))) |
| 1539 | end if |
| 1540 | |
| 1541 | if (.not. shell%in_pipeline_child) then |
| 1542 | ! Only manage process groups and terminal when NOT in a pipeline |
| 1543 | ! child. The AST pipeline executor handles these at pipeline level. |
| 1544 | pgid = pid |
| 1545 | ret = c_setpgid(pid, pgid) |
| 1546 | |
| 1547 | if (foreground) then |
| 1548 | ! Give terminal to child |
| 1549 | if (shell%is_interactive) then |
| 1550 | pgid_ret = c_tcsetpgrp(shell%shell_terminal, pgid) |
| 1551 | end if |
| 1552 | |
| 1553 | ! Wait for child |
| 1554 | ret = c_waitpid(pid, c_loc(wait_status), WUNTRACED) |
| 1555 | |
| 1556 | ! Take back terminal |
| 1557 | if (shell%is_interactive) then |
| 1558 | pgid_ret = c_tcsetpgrp(shell%shell_terminal, shell%shell_pgid) |
| 1559 | end if |
| 1560 | |
| 1561 | if (ret == pid) then |
| 1562 | if (WIFEXITED(wait_status)) then |
| 1563 | shell%last_exit_status = WEXITSTATUS(wait_status) |
| 1564 | else if (WIFSIGNALED(wait_status)) then |
| 1565 | ! Process was killed by a signal - exit status is 128 + signal_number |
| 1566 | shell%last_exit_status = 128 + WTERMSIG(wait_status) |
| 1567 | else if (WIFSTOPPED(wait_status)) then |
| 1568 | job_id = add_job(shell, pgid, original_input, .true.) |
| 1569 | ! Set state to stopped (add_job defaults to RUNNING) |
| 1570 | block |
| 1571 | integer :: ji |
| 1572 | do ji = 1, MAX_JOBS |
| 1573 | if (shell%jobs(ji)%job_id == job_id) then |
| 1574 | shell%jobs(ji)%state = JOB_STOPPED |
| 1575 | exit |
| 1576 | end if |
| 1577 | end do |
| 1578 | end block |
| 1579 | write(output_unit, '(a)') 'Stopped' |
| 1580 | end if |
| 1581 | end if |
| 1582 | else |
| 1583 | ! Background job |
| 1584 | job_id = add_job(shell, pgid, original_input, .false.) |
| 1585 | ! Only print job notification in interactive mode |
| 1586 | if (shell%is_interactive) then |
| 1587 | write(output_unit, '(a,i0,a,i0)') '[', job_id, '] ', pid |
| 1588 | end if |
| 1589 | ! Set $! to the background job PID |
| 1590 | shell%last_bg_pid = pid |
| 1591 | end if |
| 1592 | else |
| 1593 | ! In pipeline child: just wait for the grandchild, no terminal/pgroup mgmt |
| 1594 | ret = c_waitpid(pid, c_loc(wait_status), int(0, c_int)) |
| 1595 | if (ret == pid) then |
| 1596 | if (WIFEXITED(wait_status)) then |
| 1597 | shell%last_exit_status = WEXITSTATUS(wait_status) |
| 1598 | else if (WIFSIGNALED(wait_status)) then |
| 1599 | shell%last_exit_status = 128 + WTERMSIG(wait_status) |
| 1600 | end if |
| 1601 | end if |
| 1602 | end if |
| 1603 | end if |
| 1604 | end subroutine |
| 1605 | |
| 1606 | subroutine handle_heredoc(cmd, shell) |
| 1607 | type(command_t), intent(in) :: cmd |
| 1608 | type(shell_state_t), intent(inout) :: shell |
| 1609 | integer, target :: pipefd(2) |
| 1610 | integer :: ret |
| 1611 | integer(c_size_t) :: bytes_written |
| 1612 | character(kind=c_char), target :: c_content(MAX_HEREDOC_LEN) |
| 1613 | integer :: i |
| 1614 | character(len=:), allocatable :: content_to_write |
| 1615 | character(len=:), allocatable :: expanded_content |
| 1616 | |
| 1617 | ! Handle here-string (<<<) |
| 1618 | if (allocated(cmd%here_string)) then |
| 1619 | content_to_write = cmd%here_string // char(10) ! Add newline |
| 1620 | else if (allocated(cmd%heredoc_content)) then |
| 1621 | ! Check if we should expand variables (unquoted delimiter) |
| 1622 | if (.not. cmd%heredoc_quoted) then |
| 1623 | ! Expand variables in heredoc content |
| 1624 | call expand_variables(cmd%heredoc_content, expanded_content, shell) |
| 1625 | if (allocated(expanded_content)) then |
| 1626 | content_to_write = expanded_content |
| 1627 | else |
| 1628 | content_to_write = cmd%heredoc_content |
| 1629 | end if |
| 1630 | else |
| 1631 | ! Quoted delimiter - use content as-is |
| 1632 | content_to_write = cmd%heredoc_content |
| 1633 | end if |
| 1634 | else |
| 1635 | return |
| 1636 | end if |
| 1637 | |
| 1638 | ! Create pipe for input |
| 1639 | ret = c_pipe(c_loc(pipefd)) |
| 1640 | if (ret == 0) then |
| 1641 | ! Convert content to C string |
| 1642 | do i = 1, min(len(content_to_write), MAX_HEREDOC_LEN-1) |
| 1643 | c_content(i) = content_to_write(i:i) |
| 1644 | end do |
| 1645 | c_content(min(len(content_to_write), MAX_HEREDOC_LEN-1)+1) = c_null_char |
| 1646 | |
| 1647 | ! Write content to pipe |
| 1648 | bytes_written = c_write(pipefd(2), c_loc(c_content), & |
| 1649 | int(min(len(content_to_write), MAX_HEREDOC_LEN-1), c_size_t)) |
| 1650 | |
| 1651 | ! Close write end and redirect stdin to read end |
| 1652 | ret = c_close(pipefd(2)) |
| 1653 | ret = c_dup2(pipefd(1), STDIN_FD) |
| 1654 | ret = c_close(pipefd(1)) |
| 1655 | end if |
| 1656 | end subroutine |
| 1657 | |
| 1658 | subroutine setup_redirections(cmd, shell) |
| 1659 | type(command_t), intent(in) :: cmd |
| 1660 | type(shell_state_t), intent(in) :: shell |
| 1661 | integer :: fd, ret |
| 1662 | integer :: flags |
| 1663 | character(len=256), target :: c_filename |
| 1664 | |
| 1665 | ! Handle input redirection |
| 1666 | if (allocated(cmd%input_file)) then |
| 1667 | c_filename = trim(cmd%input_file)//c_null_char |
| 1668 | fd = c_open(c_loc(c_filename), O_RDONLY, 0) |
| 1669 | if (fd >= 0) then |
| 1670 | ret = c_dup2(fd, STDIN_FD) |
| 1671 | ret = c_close(fd) |
| 1672 | else |
| 1673 | write(error_unit, '(3a)') 'Cannot open input file: ', trim(cmd%input_file) |
| 1674 | call c_exit(1) |
| 1675 | end if |
| 1676 | end if |
| 1677 | |
| 1678 | ! Handle output redirection |
| 1679 | if (allocated(cmd%output_file)) then |
| 1680 | ! Check noclobber protection |
| 1681 | if (shell%option_noclobber .and. .not. cmd%force_clobber .and. .not. cmd%append_output) then |
| 1682 | ! Check if file exists |
| 1683 | if (file_exists(trim(cmd%output_file))) then |
| 1684 | write(error_unit, '(3a)') 'fortsh: ', trim(cmd%output_file), ': cannot overwrite existing file' |
| 1685 | call c_exit(1) |
| 1686 | end if |
| 1687 | end if |
| 1688 | |
| 1689 | if (cmd%append_output) then |
| 1690 | flags = ior(ior(O_WRONLY, O_CREAT), O_APPEND) |
| 1691 | else |
| 1692 | flags = ior(ior(O_WRONLY, O_CREAT), O_TRUNC) |
| 1693 | end if |
| 1694 | |
| 1695 | c_filename = trim(cmd%output_file)//c_null_char |
| 1696 | fd = c_open(c_loc(c_filename), flags, int(o'644', c_int)) |
| 1697 | if (fd >= 0) then |
| 1698 | ret = c_dup2(fd, STDOUT_FD) |
| 1699 | ret = c_close(fd) |
| 1700 | else |
| 1701 | write(error_unit, '(3a)') 'Cannot open output file: ', trim(cmd%output_file) |
| 1702 | call c_exit(1) |
| 1703 | end if |
| 1704 | end if |
| 1705 | |
| 1706 | ! Handle error redirection |
| 1707 | if (allocated(cmd%error_file)) then |
| 1708 | if (cmd%append_error) then |
| 1709 | flags = ior(ior(O_WRONLY, O_CREAT), O_APPEND) |
| 1710 | else |
| 1711 | flags = ior(ior(O_WRONLY, O_CREAT), O_TRUNC) |
| 1712 | end if |
| 1713 | |
| 1714 | c_filename = trim(cmd%error_file)//c_null_char |
| 1715 | fd = c_open(c_loc(c_filename), flags, int(o'644', c_int)) |
| 1716 | if (fd >= 0) then |
| 1717 | ret = c_dup2(fd, STDERR_FD) |
| 1718 | ret = c_close(fd) |
| 1719 | else |
| 1720 | write(error_unit, '(3a)') 'Cannot open error file: ', trim(cmd%error_file) |
| 1721 | call c_exit(1) |
| 1722 | end if |
| 1723 | end if |
| 1724 | |
| 1725 | ! Handle advanced redirections |
| 1726 | if (cmd%redirect_stderr_to_stdout) then |
| 1727 | ret = c_dup2(STDOUT_FD, STDERR_FD) |
| 1728 | end if |
| 1729 | |
| 1730 | if (cmd%redirect_stdout_to_stderr) then |
| 1731 | ret = c_dup2(STDERR_FD, STDOUT_FD) |
| 1732 | end if |
| 1733 | |
| 1734 | ! Handle FD duplications from redirections array |
| 1735 | call apply_fd_redirections(cmd) |
| 1736 | end subroutine |
| 1737 | |
| 1738 | ! Apply file descriptor redirections (including variable FD duplications) |
| 1739 | subroutine apply_fd_redirections(cmd) |
| 1740 | use expansion, only: get_environment_var |
| 1741 | type(command_t), intent(in) :: cmd |
| 1742 | character(len=:), allocatable :: expanded_value |
| 1743 | character(len=256) :: target_fd_str, var_name |
| 1744 | integer :: i, target_fd, ret, iostat, bracket_pos |
| 1745 | |
| 1746 | do i = 1, cmd%num_redirections |
| 1747 | select case(cmd%redirections(i)%type) |
| 1748 | case(REDIR_DUP_OUT) ! >&n |
| 1749 | if (allocated(cmd%redirections(i)%target_fd_expr)) then |
| 1750 | ! Variable FD expression - need to expand |
| 1751 | target_fd_str = cmd%redirections(i)%target_fd_expr |
| 1752 | |
| 1753 | ! Extract variable name from ${var} or ${var[index]} pattern |
| 1754 | if (len_trim(target_fd_str) > 3) then |
| 1755 | if (target_fd_str(1:2) == '${' .and. & |
| 1756 | target_fd_str(len_trim(target_fd_str):len_trim(target_fd_str)) == '}') then |
| 1757 | var_name = target_fd_str(3:len_trim(target_fd_str)-1) |
| 1758 | |
| 1759 | ! Check for array syntax [index] |
| 1760 | bracket_pos = index(var_name, '[') |
| 1761 | if (bracket_pos > 0) then |
| 1762 | ! For COPROC[1] style, try environment variable with underscore |
| 1763 | ! e.g., COPROC_1 for COPROC[1] |
| 1764 | var_name = var_name(:bracket_pos-1) // '_' // & |
| 1765 | var_name(bracket_pos+1:index(var_name,']')-1) |
| 1766 | end if |
| 1767 | |
| 1768 | ! Get value from environment |
| 1769 | expanded_value = get_environment_var(trim(var_name)) |
| 1770 | if (allocated(expanded_value)) then |
| 1771 | read(expanded_value, *, iostat=iostat) target_fd |
| 1772 | if (iostat == 0 .and. target_fd >= 0) then |
| 1773 | ret = c_dup2(target_fd, cmd%redirections(i)%fd) |
| 1774 | end if |
| 1775 | end if |
| 1776 | end if |
| 1777 | end if |
| 1778 | else if (cmd%redirections(i)%target_fd >= 0) then |
| 1779 | ! Literal FD |
| 1780 | ret = c_dup2(cmd%redirections(i)%target_fd, cmd%redirections(i)%fd) |
| 1781 | end if |
| 1782 | |
| 1783 | case(REDIR_DUP_IN) ! <&n |
| 1784 | if (allocated(cmd%redirections(i)%target_fd_expr)) then |
| 1785 | ! Variable FD expression - need to expand |
| 1786 | target_fd_str = cmd%redirections(i)%target_fd_expr |
| 1787 | |
| 1788 | ! Extract variable name from ${var} pattern |
| 1789 | if (len_trim(target_fd_str) > 3) then |
| 1790 | if (target_fd_str(1:2) == '${' .and. & |
| 1791 | target_fd_str(len_trim(target_fd_str):len_trim(target_fd_str)) == '}') then |
| 1792 | var_name = target_fd_str(3:len_trim(target_fd_str)-1) |
| 1793 | |
| 1794 | ! Check for array syntax [index] |
| 1795 | bracket_pos = index(var_name, '[') |
| 1796 | if (bracket_pos > 0) then |
| 1797 | ! For COPROC[0] style, try environment variable with underscore |
| 1798 | var_name = var_name(:bracket_pos-1) // '_' // & |
| 1799 | var_name(bracket_pos+1:index(var_name,']')-1) |
| 1800 | end if |
| 1801 | |
| 1802 | ! Get value from environment |
| 1803 | expanded_value = get_environment_var(trim(var_name)) |
| 1804 | if (allocated(expanded_value)) then |
| 1805 | read(expanded_value, *, iostat=iostat) target_fd |
| 1806 | if (iostat == 0 .and. target_fd >= 0) then |
| 1807 | ret = c_dup2(target_fd, cmd%redirections(i)%fd) |
| 1808 | end if |
| 1809 | end if |
| 1810 | end if |
| 1811 | end if |
| 1812 | else if (cmd%redirections(i)%target_fd >= 0) then |
| 1813 | ! Literal FD |
| 1814 | ret = c_dup2(cmd%redirections(i)%target_fd, cmd%redirections(i)%fd) |
| 1815 | end if |
| 1816 | |
| 1817 | case(REDIR_FD_OUT, REDIR_FD_APPEND) ! n> file or n>> file |
| 1818 | if (allocated(cmd%redirections(i)%filename)) then |
| 1819 | block |
| 1820 | character(len=256), target :: c_filename |
| 1821 | integer :: fd, flags |
| 1822 | |
| 1823 | c_filename = trim(cmd%redirections(i)%filename) // c_null_char |
| 1824 | |
| 1825 | if (cmd%redirections(i)%type == REDIR_FD_APPEND) then |
| 1826 | flags = ior(ior(O_WRONLY, O_CREAT), O_APPEND) |
| 1827 | else |
| 1828 | flags = ior(ior(O_WRONLY, O_CREAT), O_TRUNC) |
| 1829 | end if |
| 1830 | |
| 1831 | fd = c_open(c_loc(c_filename), flags, int(o'644', c_int)) |
| 1832 | if (fd >= 0) then |
| 1833 | ret = c_dup2(fd, cmd%redirections(i)%fd) |
| 1834 | ret = c_close(fd) |
| 1835 | end if |
| 1836 | end block |
| 1837 | end if |
| 1838 | |
| 1839 | case(REDIR_CLOSE) ! n>&- |
| 1840 | ret = c_close(cmd%redirections(i)%fd) |
| 1841 | |
| 1842 | end select |
| 1843 | end do |
| 1844 | end subroutine |
| 1845 | |
| 1846 | subroutine exec_child(tokens, num_tokens) |
| 1847 | use system_interface, only: file_exists, file_is_executable |
| 1848 | use iso_fortran_env, only: error_unit |
| 1849 | character(len=*), intent(in) :: tokens(:) |
| 1850 | integer, intent(in) :: num_tokens |
| 1851 | |
| 1852 | type(c_ptr), target :: argv(num_tokens + 1) |
| 1853 | integer :: c_tok_len |
| 1854 | character(kind=c_char), allocatable, target :: c_tokens(:,:) |
| 1855 | integer :: i, j, k |
| 1856 | integer :: ret |
| 1857 | logical :: is_path_command |
| 1858 | |
| 1859 | |
| 1860 | ! Allocate C token buffer based on actual token character length |
| 1861 | c_tok_len = len(tokens(1)) + 1 |
| 1862 | allocate(c_tokens(c_tok_len, num_tokens)) |
| 1863 | |
| 1864 | ! Convert tokens to C strings |
| 1865 | do i = 1, num_tokens |
| 1866 | ! Use len_trim, but if it's 0 and token starts with whitespace, use 1 |
| 1867 | ! This preserves whitespace-only arguments like " " |
| 1868 | j = len_trim(tokens(i)) |
| 1869 | if (j == 0 .and. len(tokens(i)) > 0) then |
| 1870 | if (tokens(i)(1:1) == ' ' .or. tokens(i)(1:1) == char(9)) then |
| 1871 | j = 1 ! Keep at least one whitespace character |
| 1872 | end if |
| 1873 | end if |
| 1874 | do k = 1, j |
| 1875 | c_tokens(k, i) = tokens(i)(k:k) |
| 1876 | end do |
| 1877 | c_tokens(j + 1, i) = c_null_char |
| 1878 | argv(i) = c_loc(c_tokens(1, i)) |
| 1879 | end do |
| 1880 | argv(num_tokens + 1) = c_null_ptr |
| 1881 | |
| 1882 | ! Check if command is a path (contains /) before calling exec |
| 1883 | ! This allows us to distinguish between "file not found" (127) and "permission denied" (126) |
| 1884 | is_path_command = (index(trim(tokens(1)), '/') > 0) |
| 1885 | |
| 1886 | if (is_path_command) then |
| 1887 | ! For path-based commands, check if file exists and is executable |
| 1888 | if (file_exists(trim(tokens(1)))) then |
| 1889 | if (.not. file_is_executable(trim(tokens(1)))) then |
| 1890 | ! File exists but is not executable -> exit 126 |
| 1891 | write(error_unit, '(a)') 'fortsh: ' // trim(tokens(1)) // ': Permission denied' |
| 1892 | call c_exit(126) |
| 1893 | end if |
| 1894 | end if |
| 1895 | ! If file doesn't exist, execvp will fail with ENOENT (exit 127 below) |
| 1896 | end if |
| 1897 | |
| 1898 | ! Execute the command |
| 1899 | ret = c_execvp(argv(1), c_loc(argv)) |
| 1900 | |
| 1901 | ! If we reach here, exec failed |
| 1902 | call show_command_not_found_error(trim(tokens(1))) |
| 1903 | end subroutine |
| 1904 | |
| 1905 | subroutine execute_function(cmd, shell) |
| 1906 | type(command_t), intent(in) :: cmd |
| 1907 | type(shell_state_t), intent(inout) :: shell |
| 1908 | |
| 1909 | type(string_t), allocatable :: function_body(:) |
| 1910 | type(string_t), allocatable :: saved_positional_params(:) |
| 1911 | integer :: saved_num_positional |
| 1912 | integer :: i |
| 1913 | type(pipeline_t) :: pipeline |
| 1914 | character(len=:), allocatable :: expanded_line |
| 1915 | logical :: function_returned |
| 1916 | |
| 1917 | ! Save current positional parameters (caller's $1, $2, etc.) |
| 1918 | if (allocated(shell%positional_params)) then |
| 1919 | allocate(saved_positional_params(size(shell%positional_params))) |
| 1920 | do i = 1, size(shell%positional_params) |
| 1921 | saved_positional_params(i)%str = shell%positional_params(i)%str |
| 1922 | end do |
| 1923 | end if |
| 1924 | saved_num_positional = shell%num_positional |
| 1925 | |
| 1926 | ! Enter function scope |
| 1927 | shell%function_depth = shell%function_depth + 1 |
| 1928 | |
| 1929 | ! Initialize local variable count for this function scope |
| 1930 | if (shell%function_depth <= size(shell%local_var_counts)) then |
| 1931 | shell%local_var_counts(shell%function_depth) = 0 |
| 1932 | end if |
| 1933 | |
| 1934 | ! Set positional parameters from function arguments |
| 1935 | ! cmd%tokens(1) is function name, cmd%tokens(2:) are arguments |
| 1936 | shell%num_positional = cmd%num_tokens - 1 |
| 1937 | do i = 1, shell%num_positional |
| 1938 | shell%positional_params(i)%str = cmd%tokens(i + 1) |
| 1939 | end do |
| 1940 | |
| 1941 | ! Get function body |
| 1942 | function_body = get_function_body(shell, cmd%tokens(1)) |
| 1943 | |
| 1944 | function_returned = .false. |
| 1945 | |
| 1946 | if (allocated(function_body)) then |
| 1947 | ! For defun-style functions (body has no $), append call args |
| 1948 | if (size(function_body) == 1 .and. cmd%num_tokens > 1 .and. & |
| 1949 | allocated(function_body(1)%str) .and. & |
| 1950 | index(function_body(1)%str, '$') == 0) then |
| 1951 | block |
| 1952 | integer :: j |
| 1953 | do j = 2, cmd%num_tokens |
| 1954 | function_body(1)%str = trim(function_body(1)%str) // & |
| 1955 | ' ' // trim(cmd%tokens(j)) |
| 1956 | end do |
| 1957 | end block |
| 1958 | end if |
| 1959 | |
| 1960 | ! Execute each line of the function |
| 1961 | do i = 1, size(function_body) |
| 1962 | if (allocated(function_body(i)%str) .and. len_trim(function_body(i)%str) > 0) then |
| 1963 | ! Expand aliases |
| 1964 | call expand_alias(shell, trim(function_body(i)%str), expanded_line) |
| 1965 | |
| 1966 | ! Parse and execute |
| 1967 | call parse_pipeline(expanded_line, pipeline) |
| 1968 | if (pipeline%num_commands > 0) then |
| 1969 | call execute_pipeline(pipeline, shell, expanded_line) |
| 1970 | |
| 1971 | ! NOTE: Loop replay now happens inline when 'done' is processed (see line ~470) |
| 1972 | ! Deferred replay is no longer needed for normal loop execution |
| 1973 | ! This fallback might still be needed for edge cases, so leaving it commented: |
| 1974 | ! if (shell%control_depth == 0 .or. .not. shell%control_stack(shell%control_depth)%capturing_loop_body) then |
| 1975 | ! call replay_loop_if_needed(shell) |
| 1976 | ! end if |
| 1977 | end if |
| 1978 | |
| 1979 | ! Clean up |
| 1980 | if (allocated(pipeline%commands)) then |
| 1981 | deallocate(pipeline%commands) |
| 1982 | end if |
| 1983 | |
| 1984 | ! Check if function returned early (via return builtin) |
| 1985 | ! We'll use a special flag in shell state for this |
| 1986 | if (shell%function_return_pending) then |
| 1987 | shell%function_return_pending = .false. |
| 1988 | function_returned = .true. |
| 1989 | exit |
| 1990 | end if |
| 1991 | |
| 1992 | ! Exit early if shell stopped |
| 1993 | if (.not. shell%running) exit |
| 1994 | end if |
| 1995 | end do |
| 1996 | end if |
| 1997 | |
| 1998 | ! Clean up local variables for this function scope |
| 1999 | if (shell%function_depth > 0 .and. shell%function_depth <= size(shell%local_var_counts)) then |
| 2000 | shell%local_var_counts(shell%function_depth) = 0 |
| 2001 | end if |
| 2002 | |
| 2003 | ! Exit function scope |
| 2004 | shell%function_depth = shell%function_depth - 1 |
| 2005 | |
| 2006 | ! Restore caller's positional parameters |
| 2007 | if (allocated(saved_positional_params)) then |
| 2008 | do i = 1, size(saved_positional_params) |
| 2009 | shell%positional_params(i)%str = saved_positional_params(i)%str |
| 2010 | end do |
| 2011 | deallocate(saved_positional_params) |
| 2012 | end if |
| 2013 | shell%num_positional = saved_num_positional |
| 2014 | end subroutine |
| 2015 | |
| 2016 | ! =========================================================================== |
| 2017 | ! COMPLETION FUNCTION EXECUTOR |
| 2018 | ! Called by completion module to execute -F completion functions |
| 2019 | ! =========================================================================== |
| 2020 | subroutine execute_completion_function(shell, func_name, command, word, prev_word) |
| 2021 | use variables, only: set_shell_variable, get_function_body, is_function |
| 2022 | type(shell_state_t), intent(inout) :: shell |
| 2023 | character(len=*), intent(in) :: func_name |
| 2024 | character(len=*), intent(in) :: command |
| 2025 | character(len=*), intent(in) :: word |
| 2026 | character(len=*), intent(in) :: prev_word |
| 2027 | |
| 2028 | type(command_t) :: cmd |
| 2029 | |
| 2030 | ! Check if function exists |
| 2031 | if (.not. is_function(shell, func_name)) then |
| 2032 | return |
| 2033 | end if |
| 2034 | |
| 2035 | ! Set up COMP_* variables for the completion function |
| 2036 | ! COMP_LINE - the full command line (simplified) |
| 2037 | call set_shell_variable(shell, 'COMP_LINE', trim(command) // ' ' // trim(word)) |
| 2038 | ! COMP_POINT - cursor position |
| 2039 | call set_shell_variable(shell, 'COMP_POINT', '0') |
| 2040 | ! COMP_CWORD - index of word containing cursor (0-based) |
| 2041 | call set_shell_variable(shell, 'COMP_CWORD', '1') |
| 2042 | |
| 2043 | ! Build command structure to execute the function |
| 2044 | ! The function receives: command word prev_word |
| 2045 | allocate(character(len=256) :: cmd%tokens(4)) |
| 2046 | cmd%num_tokens = 4 |
| 2047 | cmd%tokens(1) = trim(func_name) |
| 2048 | cmd%tokens(2) = trim(command) |
| 2049 | cmd%tokens(3) = trim(word) |
| 2050 | cmd%tokens(4) = trim(prev_word) |
| 2051 | |
| 2052 | ! Execute the function |
| 2053 | call execute_function(cmd, shell) |
| 2054 | |
| 2055 | ! Clean up |
| 2056 | deallocate(cmd%tokens) |
| 2057 | end subroutine execute_completion_function |
| 2058 | |
| 2059 | ! Execute eval builtin (moved here to avoid circular dependency with builtins module) |
| 2060 | subroutine execute_eval_builtin(cmd, shell) |
| 2061 | type(command_t), intent(in) :: cmd |
| 2062 | type(shell_state_t), intent(inout) :: shell |
| 2063 | |
| 2064 | character(len=4096) :: eval_command |
| 2065 | integer :: i |
| 2066 | type(pipeline_t) :: pipeline |
| 2067 | |
| 2068 | ! If no arguments, just return success |
| 2069 | if (cmd%num_tokens < 2) then |
| 2070 | shell%last_exit_status = 0 |
| 2071 | return |
| 2072 | end if |
| 2073 | |
| 2074 | ! Concatenate all arguments into a single command string |
| 2075 | eval_command = trim(cmd%tokens(2)) |
| 2076 | do i = 3, cmd%num_tokens |
| 2077 | eval_command = trim(eval_command) // ' ' // trim(cmd%tokens(i)) |
| 2078 | end do |
| 2079 | |
| 2080 | ! Parse the concatenated string as a pipeline |
| 2081 | call parse_pipeline(trim(eval_command), pipeline) |
| 2082 | |
| 2083 | ! Execute the parsed pipeline in the current shell context |
| 2084 | if (pipeline%num_commands > 0) then |
| 2085 | call execute_pipeline(pipeline, shell, trim(eval_command)) |
| 2086 | |
| 2087 | ! Clean up pipeline allocations |
| 2088 | do i = 1, pipeline%num_commands |
| 2089 | if (allocated(pipeline%commands(i)%tokens)) deallocate(pipeline%commands(i)%tokens) |
| 2090 | if (allocated(pipeline%commands(i)%input_file)) deallocate(pipeline%commands(i)%input_file) |
| 2091 | if (allocated(pipeline%commands(i)%output_file)) deallocate(pipeline%commands(i)%output_file) |
| 2092 | if (allocated(pipeline%commands(i)%error_file)) deallocate(pipeline%commands(i)%error_file) |
| 2093 | if (allocated(pipeline%commands(i)%heredoc_delimiter)) deallocate(pipeline%commands(i)%heredoc_delimiter) |
| 2094 | if (allocated(pipeline%commands(i)%heredoc_content)) deallocate(pipeline%commands(i)%heredoc_content) |
| 2095 | if (allocated(pipeline%commands(i)%here_string)) deallocate(pipeline%commands(i)%here_string) |
| 2096 | end do |
| 2097 | |
| 2098 | if (allocated(pipeline%commands)) deallocate(pipeline%commands) |
| 2099 | else |
| 2100 | ! Empty command - success |
| 2101 | shell%last_exit_status = 0 |
| 2102 | end if |
| 2103 | end subroutine |
| 2104 | |
| 2105 | ! Replay loop body if needed |
| 2106 | subroutine replay_loop_if_needed(shell) |
| 2107 | use parser, only: parse_pipeline |
| 2108 | use control_flow, only: process_control_flow |
| 2109 | type(shell_state_t), intent(inout) :: shell |
| 2110 | type(pipeline_t) :: pipeline, done_pipeline |
| 2111 | type(command_t) :: done_cmd |
| 2112 | integer :: i, iteration_count, loop_depth |
| 2113 | logical :: should_execute |
| 2114 | character(len=4) :: done_str |
| 2115 | |
| 2116 | ! Check if we should replay |
| 2117 | if (shell%control_depth == 0) return |
| 2118 | if (shell%control_stack(shell%control_depth)%loop_body_count == 0) return |
| 2119 | |
| 2120 | ! Save the loop's control depth - this won't change even if nested control structures are executed |
| 2121 | loop_depth = shell%control_depth |
| 2122 | iteration_count = 0 |
| 2123 | done_str = 'done' |
| 2124 | |
| 2125 | ! Keep replaying until loop condition is false |
| 2126 | ! Use loop_depth instead of shell%control_depth since depth can change during loop body execution |
| 2127 | do while (shell%control_depth >= loop_depth .and. shell%control_stack(loop_depth)%loop_body_count > 0) |
| 2128 | iteration_count = iteration_count + 1 |
| 2129 | if (iteration_count > 1000) then |
| 2130 | write(error_unit, '(a)') 'Loop limit reached (1000 iterations)' |
| 2131 | exit |
| 2132 | end if |
| 2133 | |
| 2134 | ! Temporarily stop capturing to avoid re-capturing during replay |
| 2135 | shell%control_stack(loop_depth)%capturing_loop_body = .false. |
| 2136 | |
| 2137 | do i = 1, shell%control_stack(loop_depth)%loop_body_count |
| 2138 | ! Check if break was requested |
| 2139 | if (shell%control_stack(loop_depth)%break_requested) then |
| 2140 | if (shell%control_stack(loop_depth)%break_level > 1) then |
| 2141 | ! Multi-level break: propagate to parent loop |
| 2142 | if (loop_depth > 1) then |
| 2143 | shell%control_stack(loop_depth - 1)%break_requested = .true. |
| 2144 | shell%control_stack(loop_depth - 1)%break_level = & |
| 2145 | shell%control_stack(loop_depth)%break_level - 1 |
| 2146 | end if |
| 2147 | end if |
| 2148 | ! Clear the break flag for this level |
| 2149 | shell%control_stack(loop_depth)%break_requested = .false. |
| 2150 | shell%control_stack(loop_depth)%break_level = 0 |
| 2151 | ! Exit the loop immediately |
| 2152 | shell%control_stack(loop_depth)%loop_body_count = 0 ! Signal loop end |
| 2153 | exit |
| 2154 | end if |
| 2155 | |
| 2156 | ! Check if continue was requested |
| 2157 | if (shell%control_stack(loop_depth)%continue_requested) then |
| 2158 | if (shell%control_stack(loop_depth)%continue_level > 1) then |
| 2159 | ! Multi-level continue: propagate to parent loop |
| 2160 | if (loop_depth > 1) then |
| 2161 | shell%control_stack(loop_depth - 1)%continue_requested = .true. |
| 2162 | shell%control_stack(loop_depth - 1)%continue_level = & |
| 2163 | shell%control_stack(loop_depth)%continue_level - 1 |
| 2164 | end if |
| 2165 | end if |
| 2166 | ! Clear the continue flag for this level |
| 2167 | shell%control_stack(loop_depth)%continue_requested = .false. |
| 2168 | shell%control_stack(loop_depth)%continue_level = 0 |
| 2169 | ! Skip the rest of the iteration |
| 2170 | exit |
| 2171 | end if |
| 2172 | |
| 2173 | call parse_pipeline(shell%control_stack(loop_depth)%loop_body(i)%str, pipeline) |
| 2174 | if (pipeline%num_commands > 0) then |
| 2175 | call execute_pipeline(pipeline, shell, shell%control_stack(loop_depth)%loop_body(i)%str) |
| 2176 | end if |
| 2177 | end do |
| 2178 | |
| 2179 | ! Re-enable capturing for next iteration |
| 2180 | shell%control_stack(loop_depth)%capturing_loop_body = .true. |
| 2181 | |
| 2182 | ! Check if loop_body_count is 0 (break was called) |
| 2183 | if (shell%control_stack(loop_depth)%loop_body_count == 0) then |
| 2184 | ! Break was called, exit the loop |
| 2185 | exit |
| 2186 | end if |
| 2187 | |
| 2188 | ! Simulate 'done' to check loop condition and update state |
| 2189 | call parse_pipeline(done_str, done_pipeline) |
| 2190 | if (done_pipeline%num_commands > 0) then |
| 2191 | done_cmd = done_pipeline%commands(1) |
| 2192 | call process_control_flow(done_cmd, shell, should_execute) |
| 2193 | ! If control_depth decreased below loop_depth, loop ended |
| 2194 | if (shell%control_depth < loop_depth) then |
| 2195 | exit |
| 2196 | end if |
| 2197 | ! Also exit if loop_body_count became 0 (break was called) |
| 2198 | if (shell%control_stack(loop_depth)%loop_body_count == 0) then |
| 2199 | exit |
| 2200 | end if |
| 2201 | else |
| 2202 | exit ! Couldn't parse done, exit |
| 2203 | end if |
| 2204 | end do |
| 2205 | end subroutine |
| 2206 | |
| 2207 | ! Reconstruct command line from command tokens |
| 2208 | subroutine reconstruct_command_from_tokens(cmd, result) |
| 2209 | type(command_t), intent(in) :: cmd |
| 2210 | character(len=*), intent(out) :: result |
| 2211 | integer :: i |
| 2212 | |
| 2213 | result = '' |
| 2214 | if (.not. allocated(cmd%tokens) .or. cmd%num_tokens == 0) return |
| 2215 | |
| 2216 | ! Join tokens with spaces |
| 2217 | do i = 1, cmd%num_tokens |
| 2218 | if (i == 1) then |
| 2219 | result = trim(cmd%tokens(i)) |
| 2220 | else |
| 2221 | result = trim(result) // ' ' // trim(cmd%tokens(i)) |
| 2222 | end if |
| 2223 | end do |
| 2224 | end subroutine |
| 2225 | |
| 2226 | ! Strip surrounding quotes (single or double) from a string |
| 2227 | ! Preserves trailing spaces within quotes |
| 2228 | subroutine strip_quotes_local(str) |
| 2229 | character(len=*), intent(inout) :: str |
| 2230 | integer :: i, j, len_str, closing_quote_pos |
| 2231 | character(len=len(str)) :: temp |
| 2232 | character(len=1) :: quote_char |
| 2233 | logical :: is_double_quote |
| 2234 | |
| 2235 | len_str = len_trim(str) |
| 2236 | if (len_str < 2) return |
| 2237 | |
| 2238 | ! Check if string starts with a quote |
| 2239 | if (str(1:1) /= "'" .and. str(1:1) /= '"') return |
| 2240 | |
| 2241 | quote_char = str(1:1) |
| 2242 | is_double_quote = (quote_char == '"') |
| 2243 | |
| 2244 | ! Search for matching closing quote (search backwards from end) |
| 2245 | closing_quote_pos = 0 |
| 2246 | do i = len_str, 2, -1 |
| 2247 | if (str(i:i) == quote_char) then |
| 2248 | closing_quote_pos = i |
| 2249 | exit |
| 2250 | end if |
| 2251 | end do |
| 2252 | |
| 2253 | ! If we found a matching closing quote, extract the content (preserving all characters including trailing spaces) |
| 2254 | if (closing_quote_pos > 1) then |
| 2255 | ! Save the original string first |
| 2256 | temp = str |
| 2257 | ! Clear the output string |
| 2258 | str = repeat(' ', len(str)) |
| 2259 | |
| 2260 | ! If double quotes, process escape sequences while copying |
| 2261 | if (is_double_quote) then |
| 2262 | i = 2 |
| 2263 | j = 1 |
| 2264 | do while (i < closing_quote_pos) |
| 2265 | if (temp(i:i) == '\' .and. i+1 < closing_quote_pos) then |
| 2266 | ! Check if this backslash escapes a special character |
| 2267 | if (temp(i+1:i+1) == '"' .or. temp(i+1:i+1) == '\' .or. & |
| 2268 | temp(i+1:i+1) == '$' .or. temp(i+1:i+1) == '`') then |
| 2269 | ! Skip backslash, keep the escaped character |
| 2270 | i = i + 1 |
| 2271 | str(j:j) = temp(i:i) |
| 2272 | i = i + 1 |
| 2273 | j = j + 1 |
| 2274 | else |
| 2275 | ! Backslash doesn't escape anything special - keep both |
| 2276 | str(j:j) = temp(i:i) |
| 2277 | i = i + 1 |
| 2278 | j = j + 1 |
| 2279 | end if |
| 2280 | else |
| 2281 | ! Regular character |
| 2282 | str(j:j) = temp(i:i) |
| 2283 | i = i + 1 |
| 2284 | j = j + 1 |
| 2285 | end if |
| 2286 | end do |
| 2287 | else |
| 2288 | ! Single quotes - copy literally without escape processing |
| 2289 | do i = 2, closing_quote_pos - 1 |
| 2290 | str(i-1:i-1) = temp(i:i) |
| 2291 | end do |
| 2292 | end if |
| 2293 | end if |
| 2294 | end subroutine |
| 2295 | |
| 2296 | ! Execute a pending trap command (set by signal_handling module) |
| 2297 | subroutine execute_pending_trap(shell) |
| 2298 | use trap_dispatch, only: eval_trap_string |
| 2299 | type(shell_state_t), intent(inout) :: shell |
| 2300 | integer :: saved_status, exit_code |
| 2301 | logical :: saved_bypass |
| 2302 | |
| 2303 | ! Save the trap command and signal before clearing |
| 2304 | character(len=:), allocatable :: trap_cmd |
| 2305 | trap_cmd = trim(shell%pending_trap_command) |
| 2306 | |
| 2307 | ! Save current exit status (traps don't affect $?) |
| 2308 | saved_status = shell%last_exit_status |
| 2309 | |
| 2310 | ! Save and clear bypass_functions — trap handlers should see all functions |
| 2311 | ! even when fired inside 'command' builtin context |
| 2312 | saved_bypass = shell%bypass_functions |
| 2313 | shell%bypass_functions = .false. |
| 2314 | |
| 2315 | ! Clear the pending trap |
| 2316 | shell%pending_trap_command = '' |
| 2317 | shell%pending_trap_signal = 0 |
| 2318 | |
| 2319 | ! Set flag to prevent recursive trap execution |
| 2320 | shell%executing_trap = .true. |
| 2321 | |
| 2322 | ! Execute via trap_dispatch (registered by ast_executor at startup) |
| 2323 | call eval_trap_string(trim(trap_cmd), shell, exit_code) |
| 2324 | |
| 2325 | ! Clear flag to allow future trap execution |
| 2326 | shell%executing_trap = .false. |
| 2327 | |
| 2328 | ! Restore bypass_functions and exit status |
| 2329 | shell%bypass_functions = saved_bypass |
| 2330 | shell%last_exit_status = saved_status |
| 2331 | end subroutine |
| 2332 | |
| 2333 | ! Execute inline commands after "then" in single-line if statements |
| 2334 | ! Example: if [ 1 -eq 1 ]; then echo "test"; fi |
| 2335 | ! After parsing, the "then echo 'test'" becomes a single command with tokens ["then", "echo", "test"] |
| 2336 | subroutine execute_inline_then_commands(cmd, shell) |
| 2337 | type(command_t), intent(in) :: cmd |
| 2338 | type(shell_state_t), intent(inout) :: shell |
| 2339 | character(len=:), allocatable :: remainder_cmd |
| 2340 | type(pipeline_t) :: inline_pipeline |
| 2341 | integer :: i |
| 2342 | |
| 2343 | ! Only execute if we're in a truthy if block |
| 2344 | ! The control flow state has already been updated by process_control_flow |
| 2345 | if (shell%control_depth == 0) return |
| 2346 | if (.not. shell%control_stack(shell%control_depth)%should_execute) return |
| 2347 | |
| 2348 | ! Build command string from tokens 2 onwards |
| 2349 | remainder_cmd = '' |
| 2350 | do i = 2, cmd%num_tokens |
| 2351 | if (len_trim(remainder_cmd) > 0) then |
| 2352 | remainder_cmd = trim(remainder_cmd) // ' ' // trim(cmd%tokens(i)) |
| 2353 | else |
| 2354 | remainder_cmd = trim(cmd%tokens(i)) |
| 2355 | end if |
| 2356 | end do |
| 2357 | |
| 2358 | ! Parse and execute the inline commands |
| 2359 | if (len_trim(remainder_cmd) > 0) then |
| 2360 | call parse_pipeline(trim(remainder_cmd), inline_pipeline) |
| 2361 | if (inline_pipeline%num_commands > 0) then |
| 2362 | call execute_pipeline(inline_pipeline, shell, trim(remainder_cmd)) |
| 2363 | |
| 2364 | ! Clean up pipeline allocations |
| 2365 | do i = 1, inline_pipeline%num_commands |
| 2366 | if (allocated(inline_pipeline%commands(i)%tokens)) deallocate(inline_pipeline%commands(i)%tokens) |
| 2367 | if (allocated(inline_pipeline%commands(i)%input_file)) deallocate(inline_pipeline%commands(i)%input_file) |
| 2368 | if (allocated(inline_pipeline%commands(i)%output_file)) deallocate(inline_pipeline%commands(i)%output_file) |
| 2369 | if (allocated(inline_pipeline%commands(i)%error_file)) deallocate(inline_pipeline%commands(i)%error_file) |
| 2370 | if (allocated(inline_pipeline%commands(i)%heredoc_delimiter)) deallocate(inline_pipeline%commands(i)%heredoc_delimiter) |
| 2371 | if (allocated(inline_pipeline%commands(i)%heredoc_content)) deallocate(inline_pipeline%commands(i)%heredoc_content) |
| 2372 | if (allocated(inline_pipeline%commands(i)%here_string)) deallocate(inline_pipeline%commands(i)%here_string) |
| 2373 | end do |
| 2374 | |
| 2375 | if (allocated(inline_pipeline%commands)) deallocate(inline_pipeline%commands) |
| 2376 | end if |
| 2377 | end if |
| 2378 | end subroutine |
| 2379 | |
| 2380 | ! Initialize control_flow's evaluate_condition procedure pointer |
| 2381 | ! This breaks the circular dependency by setting the pointer at runtime |
| 2382 | subroutine init_control_flow_callbacks() |
| 2383 | use control_flow |
| 2384 | evaluate_condition => evaluate_condition_impl |
| 2385 | end subroutine |
| 2386 | |
| 2387 | ! Evaluate a condition for control flow (if/while statements) |
| 2388 | subroutine evaluate_condition_impl(condition_cmd, shell, result) |
| 2389 | use test_builtin, only: execute_test_command |
| 2390 | use parser, only: parse_pipeline |
| 2391 | character(len=*), intent(in) :: condition_cmd |
| 2392 | type(shell_state_t), intent(inout) :: shell |
| 2393 | logical, intent(out) :: result |
| 2394 | |
| 2395 | type(pipeline_t) :: pipeline |
| 2396 | integer :: saved_depth, i |
| 2397 | |
| 2398 | ! POSIX: Suppress errexit during condition evaluation (if/while/until test expressions) |
| 2399 | shell%evaluating_condition = .true. |
| 2400 | |
| 2401 | ! Save control depth and temporarily reset to 0 so condition executes unconditionally |
| 2402 | ! This prevents should_execute_command() from blocking condition evaluation |
| 2403 | saved_depth = shell%control_depth |
| 2404 | shell%control_depth = 0 |
| 2405 | |
| 2406 | ! Check if it's a test command (starts with [ or test) |
| 2407 | if (index(trim(condition_cmd), '[') == 1 .or. & |
| 2408 | index(trim(condition_cmd), 'test ') == 1) then |
| 2409 | call execute_test_condition_impl(condition_cmd, shell, result) |
| 2410 | else |
| 2411 | ! For any other command, parse and execute it to get exit status |
| 2412 | call parse_pipeline(trim(condition_cmd), pipeline) |
| 2413 | |
| 2414 | if (pipeline%num_commands > 0) then |
| 2415 | call execute_pipeline(pipeline, shell, trim(condition_cmd)) |
| 2416 | |
| 2417 | ! Clean up allocations |
| 2418 | do i = 1, pipeline%num_commands |
| 2419 | if (allocated(pipeline%commands(i)%tokens)) deallocate(pipeline%commands(i)%tokens) |
| 2420 | if (allocated(pipeline%commands(i)%input_file)) deallocate(pipeline%commands(i)%input_file) |
| 2421 | if (allocated(pipeline%commands(i)%output_file)) deallocate(pipeline%commands(i)%output_file) |
| 2422 | if (allocated(pipeline%commands(i)%error_file)) deallocate(pipeline%commands(i)%error_file) |
| 2423 | if (allocated(pipeline%commands(i)%heredoc_delimiter)) deallocate(pipeline%commands(i)%heredoc_delimiter) |
| 2424 | if (allocated(pipeline%commands(i)%heredoc_content)) deallocate(pipeline%commands(i)%heredoc_content) |
| 2425 | if (allocated(pipeline%commands(i)%here_string)) deallocate(pipeline%commands(i)%here_string) |
| 2426 | end do |
| 2427 | |
| 2428 | if (allocated(pipeline%commands)) deallocate(pipeline%commands) |
| 2429 | |
| 2430 | result = (shell%last_exit_status == 0) |
| 2431 | else |
| 2432 | result = .false. |
| 2433 | end if |
| 2434 | end if |
| 2435 | |
| 2436 | ! Restore control depth |
| 2437 | shell%control_depth = saved_depth |
| 2438 | |
| 2439 | ! Re-enable errexit checking |
| 2440 | shell%evaluating_condition = .false. |
| 2441 | end subroutine |
| 2442 | |
| 2443 | ! Simple tokenization by spaces |
| 2444 | subroutine tokenize_line(input, tokens, num_tokens) |
| 2445 | character(len=*), intent(in) :: input |
| 2446 | character(len=256), intent(out) :: tokens(:) |
| 2447 | integer, intent(out) :: num_tokens |
| 2448 | integer :: i, start_pos |
| 2449 | |
| 2450 | num_tokens = 0 |
| 2451 | i = 1 |
| 2452 | |
| 2453 | do while (i <= len_trim(input)) |
| 2454 | ! Skip spaces |
| 2455 | do while (i <= len_trim(input) .and. input(i:i) == ' ') |
| 2456 | i = i + 1 |
| 2457 | end do |
| 2458 | |
| 2459 | if (i > len_trim(input)) exit |
| 2460 | |
| 2461 | ! Start of token |
| 2462 | start_pos = i |
| 2463 | do while (i <= len_trim(input) .and. input(i:i) /= ' ') |
| 2464 | i = i + 1 |
| 2465 | end do |
| 2466 | |
| 2467 | ! Store token |
| 2468 | num_tokens = num_tokens + 1 |
| 2469 | if (num_tokens <= size(tokens)) then |
| 2470 | tokens(num_tokens) = input(start_pos:i-1) |
| 2471 | end if |
| 2472 | end do |
| 2473 | end subroutine |
| 2474 | |
| 2475 | ! Helper for evaluating test conditions |
| 2476 | subroutine execute_test_condition_impl(condition_cmd, shell, result) |
| 2477 | use test_builtin, only: execute_test_command |
| 2478 | use control_flow, only: simple_variable_expand |
| 2479 | character(len=*), intent(in) :: condition_cmd |
| 2480 | type(shell_state_t), intent(inout) :: shell |
| 2481 | logical, intent(out) :: result |
| 2482 | |
| 2483 | type(command_t) :: cmd |
| 2484 | character(len=256) :: tokens(50), expanded_token |
| 2485 | character(len=:), allocatable :: expanded_result |
| 2486 | integer :: num_tokens, i |
| 2487 | |
| 2488 | ! Tokenize the condition command |
| 2489 | num_tokens = 0 |
| 2490 | call tokenize_line(trim(condition_cmd), tokens, num_tokens) |
| 2491 | |
| 2492 | ! Allocate and set command tokens with variable expansion |
| 2493 | if (allocated(cmd%tokens)) deallocate(cmd%tokens) |
| 2494 | allocate(character(len=256) :: cmd%tokens(num_tokens)) |
| 2495 | cmd%num_tokens = num_tokens |
| 2496 | do i = 1, num_tokens |
| 2497 | expanded_token = tokens(i) |
| 2498 | |
| 2499 | ! Expand variables in the token (e.g., $count becomes the value of count) |
| 2500 | if (index(expanded_token, '$') > 0) then |
| 2501 | call simple_variable_expand(expanded_token, expanded_result, shell) |
| 2502 | if (allocated(expanded_result)) then |
| 2503 | cmd%tokens(i) = expanded_result |
| 2504 | else |
| 2505 | cmd%tokens(i) = expanded_token |
| 2506 | end if |
| 2507 | else |
| 2508 | cmd%tokens(i) = expanded_token |
| 2509 | end if |
| 2510 | end do |
| 2511 | |
| 2512 | ! Execute the test command |
| 2513 | call execute_test_command(cmd, shell) |
| 2514 | |
| 2515 | ! Clean up |
| 2516 | if (allocated(cmd%tokens)) deallocate(cmd%tokens) |
| 2517 | |
| 2518 | result = (shell%last_exit_status == 0) |
| 2519 | end subroutine |
| 2520 | |
| 2521 | ! Check if command is a function definition and register it |
| 2522 | function is_function_definition_command(cmd, shell) result(is_func_def) |
| 2523 | use variables, only: add_function |
| 2524 | type(command_t), intent(in) :: cmd |
| 2525 | type(shell_state_t), intent(inout) :: shell |
| 2526 | logical :: is_func_def |
| 2527 | |
| 2528 | character(len=256) :: func_name |
| 2529 | character(len=2048) :: reconstructed |
| 2530 | character(len=:), allocatable :: func_body_line |
| 2531 | integer :: body_count, brace_start, brace_end, paren_pos |
| 2532 | |
| 2533 | is_func_def = .false. |
| 2534 | |
| 2535 | ! Check if we have enough tokens |
| 2536 | if (cmd%num_tokens < 1) return |
| 2537 | |
| 2538 | ! IMPORTANT: Don't treat quoted strings as function definitions |
| 2539 | ! If the first token is quoted, it cannot be a function definition |
| 2540 | if (allocated(cmd%token_quoted) .and. size(cmd%token_quoted) >= 1) then |
| 2541 | if (cmd%token_quoted(1)) return |
| 2542 | end if |
| 2543 | |
| 2544 | ! Reconstruct the full command to analyze it |
| 2545 | call reconstruct_command_from_tokens(cmd, reconstructed) |
| 2546 | |
| 2547 | ! Check for pattern: name() { body } |
| 2548 | ! Look for () followed by { |
| 2549 | paren_pos = index(reconstructed, '()') |
| 2550 | if (paren_pos == 0) return |
| 2551 | |
| 2552 | ! Extract function name (everything before ()) |
| 2553 | func_name = adjustl(reconstructed(1:paren_pos-1)) |
| 2554 | if (len_trim(func_name) == 0) return |
| 2555 | |
| 2556 | ! IMPORTANT: Function names cannot contain spaces |
| 2557 | ! This prevents "echo 'a() { }'" from being treated as a function definition |
| 2558 | ! (it would reconstruct to "echo a() { }" with func_name="echo a") |
| 2559 | if (index(func_name, ' ') > 0) return |
| 2560 | |
| 2561 | ! Check if there's a { after the () |
| 2562 | brace_start = index(reconstructed(paren_pos:), '{') |
| 2563 | if (brace_start == 0) return |
| 2564 | brace_start = paren_pos + brace_start - 1 |
| 2565 | |
| 2566 | ! Find matching } |
| 2567 | brace_end = index(reconstructed(brace_start:), '}') |
| 2568 | if (brace_end == 0) return |
| 2569 | brace_end = brace_start + brace_end - 1 |
| 2570 | |
| 2571 | ! Extract function body (between { and }) |
| 2572 | if (brace_end > brace_start + 1) then |
| 2573 | body_count = 1 |
| 2574 | func_body_line = trim(adjustl(reconstructed(brace_start+1:brace_end-1))) |
| 2575 | else |
| 2576 | body_count = 0 |
| 2577 | func_body_line = '' |
| 2578 | end if |
| 2579 | |
| 2580 | ! Register the function |
| 2581 | call add_function(shell, trim(func_name), [func_body_line], body_count) |
| 2582 | |
| 2583 | is_func_def = .true. |
| 2584 | end function |
| 2585 | |
| 2586 | ! Execute subshell ( cmd1; cmd2 ) |
| 2587 | ! Forks a child process and executes commands in isolated environment |
| 2588 | subroutine execute_subshell(content, shell, original_input) |
| 2589 | character(len=*), intent(in) :: content |
| 2590 | type(shell_state_t), intent(inout) :: shell |
| 2591 | character(len=*), intent(in) :: original_input |
| 2592 | |
| 2593 | integer(c_pid_t) :: pid |
| 2594 | integer(c_int), target :: wait_status |
| 2595 | integer :: ret |
| 2596 | type(pipeline_t) :: subshell_pipeline |
| 2597 | |
| 2598 | if (.false.) print *, original_input ! Silence unused warning |
| 2599 | |
| 2600 | pid = c_fork() |
| 2601 | |
| 2602 | if (pid < 0) then |
| 2603 | write(error_unit, '(a)') 'Error: fork failed for subshell' |
| 2604 | shell%last_exit_status = 1 |
| 2605 | else if (pid == 0) then |
| 2606 | ! Child process (subshell) |
| 2607 | ! Parse and execute the subshell content |
| 2608 | call parse_pipeline(content, subshell_pipeline) |
| 2609 | if (subshell_pipeline%num_commands > 0) then |
| 2610 | call execute_pipeline(subshell_pipeline, shell, content) |
| 2611 | end if |
| 2612 | ! Exit with the last command's exit status |
| 2613 | call c_exit(int(shell%last_exit_status, c_int)) |
| 2614 | else |
| 2615 | ! Parent process - wait for subshell |
| 2616 | shell%last_pid = pid |
| 2617 | ret = c_waitpid(pid, c_loc(wait_status), 0) |
| 2618 | |
| 2619 | if (WIFEXITED(wait_status)) then |
| 2620 | shell%last_exit_status = WEXITSTATUS(wait_status) |
| 2621 | else if (WIFSIGNALED(wait_status)) then |
| 2622 | shell%last_exit_status = 128 + WTERMSIG(wait_status) |
| 2623 | else |
| 2624 | shell%last_exit_status = 1 |
| 2625 | end if |
| 2626 | end if |
| 2627 | end subroutine |
| 2628 | |
| 2629 | ! Apply prefix assignments to environment (VAR=value command) |
| 2630 | ! Called in child process before exec to set environment variables |
| 2631 | ! scoped to the command execution |
| 2632 | subroutine apply_prefix_assignments(cmd) |
| 2633 | type(command_t), intent(in) :: cmd |
| 2634 | integer :: i, eq_pos, ret |
| 2635 | character(len=MAX_TOKEN_LEN) :: var_name, var_value |
| 2636 | character(len=MAX_TOKEN_LEN), target :: c_var_name, c_var_value |
| 2637 | |
| 2638 | ! Iterate through all prefix assignments |
| 2639 | if (.not. allocated(cmd%prefix_assignments)) return |
| 2640 | do i = 1, cmd%num_prefix_assignments |
| 2641 | ! Find the '=' separator |
| 2642 | eq_pos = index(cmd%prefix_assignments(i), '=') |
| 2643 | |
| 2644 | if (eq_pos > 1) then |
| 2645 | ! Extract variable name and value |
| 2646 | var_name = cmd%prefix_assignments(i)(:eq_pos-1) |
| 2647 | var_value = cmd%prefix_assignments(i)(eq_pos+1:) |
| 2648 | |
| 2649 | ! Convert to C strings with null terminator |
| 2650 | c_var_name = trim(var_name)//c_null_char |
| 2651 | c_var_value = trim(var_value)//c_null_char |
| 2652 | |
| 2653 | ! Set environment variable (overwrite=1 to replace existing values) |
| 2654 | ret = c_setenv(c_loc(c_var_name), c_loc(c_var_value), 1_c_int) |
| 2655 | ! Note: We ignore the return value here since we're in a child process |
| 2656 | ! and any errors will be reflected in the command's execution |
| 2657 | end if |
| 2658 | end do |
| 2659 | end subroutine |
| 2660 | |
| 2661 | ! Process sourced files inline (for dot command in non-interactive mode) |
| 2662 | subroutine process_source_inline(shell) |
| 2663 | use variables, only: set_shell_variable |
| 2664 | type(shell_state_t), intent(inout) :: shell |
| 2665 | character(len=16384) :: input_line |
| 2666 | integer :: file_unit, iostat, i |
| 2667 | type(pipeline_t) :: pipeline |
| 2668 | character(len=:), allocatable :: expanded_line |
| 2669 | |
| 2670 | ! Reset the source flag first |
| 2671 | shell%should_source = .false. |
| 2672 | |
| 2673 | ! Open file for reading |
| 2674 | open(newunit=file_unit, file=trim(shell%source_file), status='old', action='read', iostat=iostat) |
| 2675 | if (iostat /= 0) then |
| 2676 | write(error_unit, '(a)') 'source: failed to open ' // trim(shell%source_file) |
| 2677 | shell%last_exit_status = 1 |
| 2678 | return |
| 2679 | end if |
| 2680 | |
| 2681 | ! Execute each line in the file |
| 2682 | do |
| 2683 | read(file_unit, '(a)', iostat=iostat) input_line |
| 2684 | if (iostat /= 0) exit ! End of file or error |
| 2685 | |
| 2686 | ! Skip empty lines and comments |
| 2687 | if (len_trim(input_line) == 0 .or. input_line(1:1) == '#') cycle |
| 2688 | |
| 2689 | ! Expand aliases (simple version - just use the line as-is) |
| 2690 | expanded_line = trim(input_line) |
| 2691 | |
| 2692 | ! Parse and execute pipeline |
| 2693 | call parse_pipeline(expanded_line, pipeline) |
| 2694 | |
| 2695 | if (pipeline%num_commands > 0) then |
| 2696 | call execute_pipeline(pipeline, shell, expanded_line) |
| 2697 | |
| 2698 | ! Clean up pipeline |
| 2699 | if (allocated(pipeline%commands)) then |
| 2700 | do i = 1, pipeline%num_commands |
| 2701 | if (allocated(pipeline%commands(i)%tokens)) deallocate(pipeline%commands(i)%tokens) |
| 2702 | if (allocated(pipeline%commands(i)%input_file)) deallocate(pipeline%commands(i)%input_file) |
| 2703 | if (allocated(pipeline%commands(i)%output_file)) deallocate(pipeline%commands(i)%output_file) |
| 2704 | if (allocated(pipeline%commands(i)%error_file)) deallocate(pipeline%commands(i)%error_file) |
| 2705 | if (allocated(pipeline%commands(i)%heredoc_delimiter)) deallocate(pipeline%commands(i)%heredoc_delimiter) |
| 2706 | if (allocated(pipeline%commands(i)%heredoc_content)) deallocate(pipeline%commands(i)%heredoc_content) |
| 2707 | if (allocated(pipeline%commands(i)%here_string)) deallocate(pipeline%commands(i)%here_string) |
| 2708 | end do |
| 2709 | deallocate(pipeline%commands) |
| 2710 | end if |
| 2711 | end if |
| 2712 | |
| 2713 | ! Stop execution if exit command was encountered |
| 2714 | if (.not. shell%running) exit |
| 2715 | end do |
| 2716 | |
| 2717 | close(file_unit) |
| 2718 | shell%source_file = '' |
| 2719 | end subroutine process_source_inline |
| 2720 | |
| 2721 | ! =========================================================================== |
| 2722 | ! COMPLETION EXECUTOR INITIALIZATION |
| 2723 | ! Registers the executor's completion function handler with the completion module |
| 2724 | ! =========================================================================== |
| 2725 | subroutine init_completion_executor() |
| 2726 | ! Register our execute_completion_function as the callback |
| 2727 | call register_completion_executor(execute_completion_function) |
| 2728 | end subroutine init_completion_executor |
| 2729 | |
| 2730 | ! Initialize token metadata arrays if not already set |
| 2731 | ! This is needed for commands parsed by the old parser path which doesn't |
| 2732 | ! populate these arrays. Inspects tokens to determine quote type and length. |
| 2733 | subroutine init_token_metadata(cmd) |
| 2734 | type(command_t), intent(inout) :: cmd |
| 2735 | integer :: i, token_len |
| 2736 | character(len=1) :: first_char, last_char |
| 2737 | |
| 2738 | if (cmd%num_tokens == 0) return |
| 2739 | |
| 2740 | ! Initialize token_quoted if not allocated |
| 2741 | if (.not. allocated(cmd%token_quoted)) then |
| 2742 | allocate(cmd%token_quoted(cmd%num_tokens)) |
| 2743 | cmd%token_quoted = .false. |
| 2744 | end if |
| 2745 | |
| 2746 | ! Initialize token_escaped if not allocated |
| 2747 | if (.not. allocated(cmd%token_escaped)) then |
| 2748 | allocate(cmd%token_escaped(cmd%num_tokens)) |
| 2749 | cmd%token_escaped = .false. |
| 2750 | end if |
| 2751 | |
| 2752 | ! Initialize token_quote_type if not allocated |
| 2753 | if (.not. allocated(cmd%token_quote_type)) then |
| 2754 | allocate(cmd%token_quote_type(cmd%num_tokens)) |
| 2755 | cmd%token_quote_type = QUOTE_NONE |
| 2756 | end if |
| 2757 | |
| 2758 | ! Initialize token_lengths if not allocated |
| 2759 | if (.not. allocated(cmd%token_lengths)) then |
| 2760 | allocate(cmd%token_lengths(cmd%num_tokens)) |
| 2761 | cmd%token_lengths = 0 |
| 2762 | end if |
| 2763 | |
| 2764 | ! Inspect each token to determine quote type and length |
| 2765 | do i = 1, cmd%num_tokens |
| 2766 | token_len = len_trim(cmd%tokens(i)) |
| 2767 | |
| 2768 | ! If quote info isn't set, try to detect from token content |
| 2769 | if (cmd%token_quote_type(i) == QUOTE_NONE .and. token_len >= 2) then |
| 2770 | first_char = cmd%tokens(i)(1:1) |
| 2771 | last_char = cmd%tokens(i)(token_len:token_len) |
| 2772 | |
| 2773 | ! Check for single-quoted token (may have sentinel markers char(2)/char(3)) |
| 2774 | ! IMPORTANT: Don't treat as syntactic quotes if token was escaped (e.g., \'a\') |
| 2775 | if (first_char == "'" .and. last_char == "'" .and. .not. cmd%token_escaped(i)) then |
| 2776 | cmd%token_quoted(i) = .true. |
| 2777 | cmd%token_quote_type(i) = QUOTE_SINGLE |
| 2778 | ! For single quotes, preserve trailing whitespace by not using len_trim |
| 2779 | ! Find actual content length between quotes |
| 2780 | cmd%token_lengths(i) = token_len |
| 2781 | else if (first_char == char(2)) then |
| 2782 | ! Single-quote sentinel marker - this token was single-quoted |
| 2783 | cmd%token_quoted(i) = .true. |
| 2784 | cmd%token_quote_type(i) = QUOTE_SINGLE |
| 2785 | cmd%token_lengths(i) = token_len |
| 2786 | ! Check for double-quoted token |
| 2787 | ! IMPORTANT: Don't treat as syntactic quotes if token was escaped (e.g., \"a\") |
| 2788 | else if (first_char == '"' .and. last_char == '"' .and. .not. cmd%token_escaped(i)) then |
| 2789 | cmd%token_quoted(i) = .true. |
| 2790 | cmd%token_quote_type(i) = QUOTE_DOUBLE |
| 2791 | ! For double-quoted, find actual length including trailing whitespace |
| 2792 | ! The content is between the quotes: token(2:token_len-1) |
| 2793 | ! We need to preserve the full quoted token including any trailing space inside |
| 2794 | cmd%token_lengths(i) = token_len |
| 2795 | else if (first_char == char(1)) then |
| 2796 | ! Double-quote sentinel marker |
| 2797 | cmd%token_quoted(i) = .true. |
| 2798 | cmd%token_quote_type(i) = QUOTE_DOUBLE |
| 2799 | cmd%token_lengths(i) = token_len |
| 2800 | else |
| 2801 | ! Unquoted token |
| 2802 | cmd%token_lengths(i) = token_len |
| 2803 | end if |
| 2804 | else if (cmd%token_lengths(i) == 0) then |
| 2805 | cmd%token_lengths(i) = token_len |
| 2806 | end if |
| 2807 | end do |
| 2808 | end subroutine init_token_metadata |
| 2809 | |
| 2810 | subroutine cache_command_path(shell, cmd_name) |
| 2811 | type(shell_state_t), intent(inout) :: shell |
| 2812 | character(len=*), intent(in) :: cmd_name |
| 2813 | character(len=MAX_PATH_LEN) :: full_path |
| 2814 | integer :: j |
| 2815 | |
| 2816 | ! Inline PATH search to avoid circular dependency with command_builtin |
| 2817 | block |
| 2818 | character(len=:), allocatable :: path_alloc |
| 2819 | character(len=4096) :: path_var |
| 2820 | character(len=:), allocatable :: path_comp, candidate |
| 2821 | character(len=MAX_PATH_LEN) :: candidate_buf |
| 2822 | integer :: spos, epos, cpos |
| 2823 | character(kind=c_char), target :: c_path(1025) |
| 2824 | integer :: ci, acc_status |
| 2825 | interface |
| 2826 | function cache_access(pathname, mode) bind(C, name="access") |
| 2827 | import :: c_char, c_int |
| 2828 | character(kind=c_char), intent(in) :: pathname(*) |
| 2829 | integer(c_int), value :: mode |
| 2830 | integer(c_int) :: cache_access |
| 2831 | end function |
| 2832 | end interface |
| 2833 | |
| 2834 | full_path = '' |
| 2835 | if (index(cmd_name, '/') > 0) return |
| 2836 | |
| 2837 | path_alloc = get_environment_var('PATH') |
| 2838 | if (allocated(path_alloc) .and. len_trim(path_alloc) > 0) then |
| 2839 | path_var = path_alloc |
| 2840 | else |
| 2841 | path_var = '/usr/bin:/bin' |
| 2842 | end if |
| 2843 | |
| 2844 | spos = 1 |
| 2845 | do while (spos <= len_trim(path_var)) |
| 2846 | cpos = index(path_var(spos:), ':') |
| 2847 | if (cpos == 0) then |
| 2848 | epos = len_trim(path_var) |
| 2849 | else |
| 2850 | epos = spos + cpos - 2 |
| 2851 | end if |
| 2852 | path_comp = path_var(spos:epos) |
| 2853 | if (len_trim(path_comp) == 0) path_comp = '.' |
| 2854 | |
| 2855 | write(candidate_buf, '(a,a,a)') trim(path_comp), '/', trim(cmd_name) |
| 2856 | candidate = trim(candidate_buf) |
| 2857 | |
| 2858 | ! Check executable via C access() |
| 2859 | do ci = 1, len_trim(candidate) |
| 2860 | c_path(ci) = candidate(ci:ci) |
| 2861 | end do |
| 2862 | c_path(len_trim(candidate) + 1) = c_null_char |
| 2863 | acc_status = cache_access(c_path, int(1, c_int)) ! X_OK = 1 |
| 2864 | if (acc_status == 0) then |
| 2865 | full_path = trim(candidate) |
| 2866 | exit |
| 2867 | end if |
| 2868 | |
| 2869 | if (cpos == 0) exit |
| 2870 | spos = spos + cpos |
| 2871 | end do |
| 2872 | end block |
| 2873 | if (len_trim(full_path) == 0) return |
| 2874 | |
| 2875 | ! Check if already in hash table — update hits |
| 2876 | do j = 1, shell%num_hashed_commands |
| 2877 | if (trim(shell%command_hash(j)%command_name) == & |
| 2878 | cmd_name) then |
| 2879 | shell%command_hash(j)%hits = & |
| 2880 | shell%command_hash(j)%hits + 1 |
| 2881 | return |
| 2882 | end if |
| 2883 | end do |
| 2884 | |
| 2885 | ! Add new entry |
| 2886 | if (shell%num_hashed_commands < & |
| 2887 | size(shell%command_hash)) then |
| 2888 | shell%num_hashed_commands = & |
| 2889 | shell%num_hashed_commands + 1 |
| 2890 | shell%command_hash(shell%num_hashed_commands) & |
| 2891 | %command_name = cmd_name |
| 2892 | shell%command_hash(shell%num_hashed_commands) & |
| 2893 | %full_path = full_path |
| 2894 | shell%command_hash(shell%num_hashed_commands) & |
| 2895 | %hits = 1 |
| 2896 | end if |
| 2897 | end subroutine cache_command_path |
| 2898 | |
| 2899 | end module executor |