| 1 | ! ============================================================================== |
| 2 | ! Module: substitution |
| 3 | ! Purpose: Enhanced command and process substitution |
| 4 | ! ============================================================================== |
| 5 | module substitution |
| 6 | use shell_types |
| 7 | use system_interface |
| 8 | use command_capture, only: execute_command_and_capture |
| 9 | use iso_fortran_env, only: output_unit, error_unit |
| 10 | use iso_c_binding |
| 11 | implicit none |
| 12 | |
| 13 | ! C function bindings for file descriptor manipulation |
| 14 | interface |
| 15 | function dup(fd) bind(c, name='dup') |
| 16 | import :: c_int |
| 17 | integer(c_int), value :: fd |
| 18 | integer(c_int) :: dup |
| 19 | end function |
| 20 | |
| 21 | function dup2(oldfd, newfd) bind(c, name='dup2') |
| 22 | import :: c_int |
| 23 | integer(c_int), value :: oldfd, newfd |
| 24 | integer(c_int) :: dup2 |
| 25 | end function |
| 26 | |
| 27 | function close(fd) bind(c, name='close') |
| 28 | import :: c_int |
| 29 | integer(c_int), value :: fd |
| 30 | integer(c_int) :: close |
| 31 | end function |
| 32 | |
| 33 | function c_fsync(fd) bind(c, name='fsync') |
| 34 | import :: c_int |
| 35 | integer(c_int), value :: fd |
| 36 | integer(c_int) :: c_fsync |
| 37 | end function |
| 38 | end interface |
| 39 | |
| 40 | ! Note: c_pipe, c_read, c_open, c_unlink, and O_* constants are provided by system_interface |
| 41 | |
| 42 | ! Process substitution file descriptors |
| 43 | type :: proc_subst_t |
| 44 | integer :: fd = -1 |
| 45 | character(len=256) :: filename = '' |
| 46 | integer(c_pid_t) :: pid = 0 |
| 47 | logical :: is_input = .true. ! true for <(), false for >() |
| 48 | logical :: active = .false. |
| 49 | end type proc_subst_t |
| 50 | |
| 51 | contains |
| 52 | |
| 53 | ! Enhanced command substitution with nested support |
| 54 | function enhanced_command_substitution(shell, input) result(output) |
| 55 | type(shell_state_t), intent(inout) :: shell |
| 56 | character(len=*), intent(in) :: input |
| 57 | character(len=:), allocatable :: output |
| 58 | |
| 59 | character(len=:), allocatable :: processed_input |
| 60 | integer :: actual_len |
| 61 | |
| 62 | output = '' |
| 63 | processed_input = input |
| 64 | |
| 65 | ! Process nested command substitutions from inside out |
| 66 | call process_nested_substitutions(shell, processed_input) |
| 67 | |
| 68 | ! Execute the final command in the current shell context |
| 69 | ! POSIX: errexit should not trigger in command substitution |
| 70 | shell%in_command_substitution = .true. |
| 71 | call execute_command_and_capture(shell, trim(processed_input), output, actual_len) |
| 72 | shell%in_command_substitution = .false. |
| 73 | |
| 74 | if (.not. allocated(output)) output = '' |
| 75 | |
| 76 | ! Remove trailing newlines only (preserve trailing spaces!) |
| 77 | do while (actual_len > 0 .and. output(actual_len:actual_len) == char(10)) |
| 78 | actual_len = actual_len - 1 |
| 79 | end do |
| 80 | if (actual_len > 0) then |
| 81 | output = output(1:actual_len) |
| 82 | else |
| 83 | output = '' |
| 84 | end if |
| 85 | end function |
| 86 | |
| 87 | subroutine process_nested_substitutions(shell, cmd_str) |
| 88 | type(shell_state_t), intent(inout) :: shell |
| 89 | character(len=*), intent(inout) :: cmd_str |
| 90 | |
| 91 | character(len=len(cmd_str)) :: result |
| 92 | integer :: i, j, paren_count, subst_start, subst_end, result_len |
| 93 | character(len=:), allocatable :: inner_cmd, inner_result |
| 94 | logical :: found_nested |
| 95 | |
| 96 | found_nested = .true. |
| 97 | |
| 98 | ! Keep processing until no more nested substitutions |
| 99 | do while (found_nested) |
| 100 | found_nested = .false. |
| 101 | result = '' |
| 102 | j = 1 ! Position in result (tracks actual content including spaces) |
| 103 | i = 1 |
| 104 | |
| 105 | do while (i <= len_trim(cmd_str)) |
| 106 | if (i < len_trim(cmd_str) - 1 .and. cmd_str(i:i+1) == '$(') then |
| 107 | ! Found start of command substitution |
| 108 | subst_start = i |
| 109 | paren_count = 1 |
| 110 | i = i + 2 |
| 111 | |
| 112 | ! Find the matching closing parenthesis (quote-aware) |
| 113 | do while (i <= len_trim(cmd_str) .and. paren_count > 0) |
| 114 | if (cmd_str(i:i) == '"') then |
| 115 | ! Skip double-quoted string |
| 116 | i = i + 1 |
| 117 | do while (i <= len_trim(cmd_str)) |
| 118 | if (cmd_str(i:i) == '\' .and. i < len_trim(cmd_str)) then |
| 119 | i = i + 2 ! Skip escaped character |
| 120 | else if (cmd_str(i:i) == '"') then |
| 121 | i = i + 1 |
| 122 | exit |
| 123 | else |
| 124 | i = i + 1 |
| 125 | end if |
| 126 | end do |
| 127 | else if (cmd_str(i:i) == "'") then |
| 128 | ! Skip single-quoted string |
| 129 | i = i + 1 |
| 130 | do while (i <= len_trim(cmd_str) .and. cmd_str(i:i) /= "'") |
| 131 | i = i + 1 |
| 132 | end do |
| 133 | if (i <= len_trim(cmd_str)) i = i + 1 |
| 134 | else if (cmd_str(i:i) == '(') then |
| 135 | paren_count = paren_count + 1 |
| 136 | i = i + 1 |
| 137 | else if (cmd_str(i:i) == ')') then |
| 138 | paren_count = paren_count - 1 |
| 139 | i = i + 1 |
| 140 | else |
| 141 | i = i + 1 |
| 142 | end if |
| 143 | end do |
| 144 | |
| 145 | if (paren_count == 0) then |
| 146 | subst_end = i - 1 |
| 147 | inner_cmd = cmd_str(subst_start+2:subst_end-1) |
| 148 | |
| 149 | ! Check if this inner command has nested substitutions |
| 150 | if (index(inner_cmd, '$(') == 0) then |
| 151 | ! No more nesting - execute this command in current shell context |
| 152 | ! POSIX: errexit should not trigger in command substitution |
| 153 | shell%in_command_substitution = .true. |
| 154 | call execute_command_and_capture(shell, inner_cmd, inner_result) |
| 155 | shell%in_command_substitution = .false. |
| 156 | result_len = len_trim(inner_result) |
| 157 | if (j + result_len - 1 <= len(result)) then |
| 158 | result(j:j+result_len-1) = inner_result(1:result_len) |
| 159 | j = j + result_len |
| 160 | end if |
| 161 | found_nested = .true. |
| 162 | else |
| 163 | ! Keep the substitution for next iteration |
| 164 | result_len = subst_end - subst_start + 1 |
| 165 | if (j + result_len - 1 <= len(result)) then |
| 166 | result(j:j+result_len-1) = cmd_str(subst_start:subst_end) |
| 167 | j = j + result_len |
| 168 | end if |
| 169 | end if |
| 170 | else |
| 171 | if (j <= len(result)) then |
| 172 | result(j:j) = cmd_str(subst_start:subst_start) |
| 173 | j = j + 1 |
| 174 | end if |
| 175 | i = subst_start + 1 |
| 176 | end if |
| 177 | else |
| 178 | ! Copy character preserving spaces |
| 179 | if (j <= len(result)) then |
| 180 | result(j:j) = cmd_str(i:i) |
| 181 | j = j + 1 |
| 182 | end if |
| 183 | i = i + 1 |
| 184 | end if |
| 185 | end do |
| 186 | |
| 187 | cmd_str = result |
| 188 | end do |
| 189 | end subroutine |
| 190 | |
| 191 | |
| 192 | ! Process substitution: <(command) and >(command) |
| 193 | function create_process_substitution(command, is_input) result(proc_subst) |
| 194 | character(len=*), intent(in) :: command |
| 195 | logical, intent(in) :: is_input |
| 196 | type(proc_subst_t) :: proc_subst |
| 197 | |
| 198 | character(len=256) :: fifo_name |
| 199 | character(len=:), allocatable :: full_cmd |
| 200 | |
| 201 | proc_subst%is_input = is_input |
| 202 | proc_subst%active = .false. |
| 203 | |
| 204 | ! Generate FIFO name |
| 205 | fifo_name = '/tmp/fortsh_fifo_' // generate_temp_suffix() |
| 206 | proc_subst%filename = fifo_name |
| 207 | |
| 208 | ! Create named pipe (FIFO) - placeholder |
| 209 | |
| 210 | if (is_input) then |
| 211 | ! <(command) - command writes to FIFO, shell reads from it |
| 212 | full_cmd = '(' // trim(command) // ') > ' // trim(fifo_name) // ' &' |
| 213 | else |
| 214 | ! >(command) - shell writes to FIFO, command reads from it |
| 215 | full_cmd = '(' // trim(command) // ') < ' // trim(fifo_name) // ' &' |
| 216 | end if |
| 217 | |
| 218 | ! Start background process - placeholder |
| 219 | proc_subst%active = .true. |
| 220 | end function |
| 221 | |
| 222 | subroutine cleanup_process_substitution(proc_subst) |
| 223 | type(proc_subst_t), intent(inout) :: proc_subst |
| 224 | |
| 225 | if (proc_subst%active) then |
| 226 | ! Remove FIFO - placeholder |
| 227 | proc_subst%active = .false. |
| 228 | proc_subst%filename = '' |
| 229 | proc_subst%fd = -1 |
| 230 | end if |
| 231 | end subroutine |
| 232 | |
| 233 | function generate_temp_suffix() result(suffix) |
| 234 | character(len=16) :: suffix |
| 235 | integer :: values(8) |
| 236 | |
| 237 | call date_and_time(values=values) |
| 238 | write(suffix, '(I4.4,I2.2,I2.2,I2.2,I2.2,I2.2)') values(1), values(2), values(3), values(5), values(6), values(7) |
| 239 | end function |
| 240 | |
| 241 | ! Brace expansion implementation |
| 242 | subroutine expand_braces(input, expanded_list, count) |
| 243 | character(len=*), intent(in) :: input |
| 244 | character(len=256), intent(out) :: expanded_list(100) |
| 245 | integer, intent(out) :: count |
| 246 | |
| 247 | integer :: brace_start, brace_end, depth, pos |
| 248 | character(len=256) :: prefix, suffix, middle_part |
| 249 | character(len=256) :: options(50) |
| 250 | integer :: option_count, i |
| 251 | |
| 252 | count = 0 |
| 253 | |
| 254 | ! Find first brace expansion |
| 255 | brace_start = index(input, '{') |
| 256 | if (brace_start == 0) then |
| 257 | count = 1 |
| 258 | expanded_list(1) = input |
| 259 | return |
| 260 | end if |
| 261 | |
| 262 | ! Find MATCHING closing brace by counting depth |
| 263 | depth = 0 |
| 264 | brace_end = 0 |
| 265 | do pos = brace_start, len_trim(input) |
| 266 | if (input(pos:pos) == '{') then |
| 267 | depth = depth + 1 |
| 268 | else if (input(pos:pos) == '}') then |
| 269 | depth = depth - 1 |
| 270 | if (depth == 0) then |
| 271 | brace_end = pos |
| 272 | exit |
| 273 | end if |
| 274 | end if |
| 275 | end do |
| 276 | |
| 277 | if (brace_end == 0) then |
| 278 | count = 1 |
| 279 | expanded_list(1) = input |
| 280 | return |
| 281 | end if |
| 282 | |
| 283 | prefix = input(:brace_start-1) |
| 284 | suffix = input(brace_end+1:) |
| 285 | middle_part = input(brace_start+1:brace_end-1) |
| 286 | |
| 287 | ! Parse comma-separated options or ranges |
| 288 | if (index(middle_part, '..') > 0) then |
| 289 | call expand_range(middle_part, options, option_count) |
| 290 | else |
| 291 | call parse_comma_list(middle_part, options, option_count) |
| 292 | end if |
| 293 | |
| 294 | ! Generate expanded strings |
| 295 | do i = 1, option_count |
| 296 | if (count < 100) then |
| 297 | count = count + 1 |
| 298 | expanded_list(count) = trim(prefix) // trim(options(i)) // trim(suffix) |
| 299 | end if |
| 300 | end do |
| 301 | |
| 302 | ! Recursively expand any remaining braces |
| 303 | if (count > 0) then |
| 304 | call recursive_brace_expansion(expanded_list, count) |
| 305 | end if |
| 306 | end subroutine |
| 307 | |
| 308 | subroutine expand_range(range_expr, options, count) |
| 309 | character(len=*), intent(in) :: range_expr |
| 310 | character(len=256), intent(out) :: options(50) |
| 311 | integer, intent(out) :: count |
| 312 | |
| 313 | integer :: dot_pos, start_val, end_val, i |
| 314 | character(len=32) :: start_str, end_str |
| 315 | |
| 316 | count = 0 |
| 317 | dot_pos = index(range_expr, '..') |
| 318 | |
| 319 | if (dot_pos == 0) return |
| 320 | |
| 321 | start_str = range_expr(:dot_pos-1) |
| 322 | end_str = range_expr(dot_pos+2:) |
| 323 | |
| 324 | ! Try numeric range first |
| 325 | read(start_str, *, iostat=i) start_val |
| 326 | if (i == 0) then |
| 327 | read(end_str, *, iostat=i) end_val |
| 328 | if (i == 0) then |
| 329 | do i = start_val, end_val |
| 330 | if (count < 50) then |
| 331 | count = count + 1 |
| 332 | write(options(count), '(I0)') i |
| 333 | end if |
| 334 | end do |
| 335 | return |
| 336 | end if |
| 337 | end if |
| 338 | |
| 339 | ! Character range (a-z) |
| 340 | if (len_trim(start_str) == 1 .and. len_trim(end_str) == 1) then |
| 341 | do i = ichar(start_str(1:1)), ichar(end_str(1:1)) |
| 342 | if (count < 50) then |
| 343 | count = count + 1 |
| 344 | options(count) = char(i) |
| 345 | end if |
| 346 | end do |
| 347 | end if |
| 348 | end subroutine |
| 349 | |
| 350 | subroutine parse_comma_list(list_str, options, count) |
| 351 | character(len=*), intent(in) :: list_str |
| 352 | character(len=256), intent(out) :: options(50) |
| 353 | integer, intent(out) :: count |
| 354 | |
| 355 | integer :: pos, start_pos, depth |
| 356 | |
| 357 | count = 0 |
| 358 | pos = 1 |
| 359 | start_pos = 1 |
| 360 | depth = 0 |
| 361 | |
| 362 | do while (pos <= len_trim(list_str)) |
| 363 | ! Track brace depth to avoid splitting on commas inside nested braces |
| 364 | if (list_str(pos:pos) == '{') then |
| 365 | depth = depth + 1 |
| 366 | else if (list_str(pos:pos) == '}') then |
| 367 | depth = depth - 1 |
| 368 | else if (list_str(pos:pos) == ',' .and. depth == 0) then |
| 369 | ! Only split on commas at depth 0 (not inside braces) |
| 370 | if (count < 50 .and. pos > start_pos) then |
| 371 | count = count + 1 |
| 372 | options(count) = list_str(start_pos:pos-1) |
| 373 | end if |
| 374 | start_pos = pos + 1 |
| 375 | end if |
| 376 | pos = pos + 1 |
| 377 | end do |
| 378 | |
| 379 | ! Handle last option |
| 380 | if (count < 50 .and. start_pos <= len_trim(list_str)) then |
| 381 | count = count + 1 |
| 382 | options(count) = list_str(start_pos:) |
| 383 | end if |
| 384 | end subroutine |
| 385 | |
| 386 | subroutine recursive_brace_expansion(list, count) |
| 387 | character(len=256), intent(inout) :: list(100) |
| 388 | integer, intent(inout) :: count |
| 389 | |
| 390 | character(len=256) :: temp_list(100), expanded_temp(100) |
| 391 | integer :: i, j, expanded_count, total_count |
| 392 | |
| 393 | total_count = 0 |
| 394 | |
| 395 | do i = 1, count |
| 396 | if (index(list(i), '{') > 0) then |
| 397 | call expand_braces(list(i), expanded_temp, expanded_count) |
| 398 | do j = 1, expanded_count |
| 399 | if (total_count < 100) then |
| 400 | total_count = total_count + 1 |
| 401 | temp_list(total_count) = expanded_temp(j) |
| 402 | end if |
| 403 | end do |
| 404 | else |
| 405 | if (total_count < 100) then |
| 406 | total_count = total_count + 1 |
| 407 | temp_list(total_count) = list(i) |
| 408 | end if |
| 409 | end if |
| 410 | end do |
| 411 | |
| 412 | count = total_count |
| 413 | list(1:count) = temp_list(1:count) |
| 414 | end subroutine |
| 415 | |
| 416 | ! =========================================================================== |
| 417 | ! Process Substitution FIFO Management |
| 418 | ! =========================================================================== |
| 419 | |
| 420 | ! Generate a unique FIFO path in /tmp |
| 421 | function generate_fifo_path(shell) result(fifo_path) |
| 422 | type(shell_state_t), intent(in) :: shell |
| 423 | character(len=MAX_PATH_LEN) :: fifo_path |
| 424 | character(len=32) :: suffix |
| 425 | |
| 426 | ! Create unique suffix based on PID, timestamp, and counter |
| 427 | suffix = generate_temp_suffix() |
| 428 | write(fifo_path, '(A,I0,A,A,A,I0)') '/tmp/fortsh_fifo_', shell%shell_pid, '_', & |
| 429 | trim(suffix), '_', shell%num_proc_subst_fifos |
| 430 | end function |
| 431 | |
| 432 | ! Create a FIFO and track it in shell state |
| 433 | function create_fifo_for_subst(shell, is_input) result(fifo_path) |
| 434 | type(shell_state_t), intent(inout) :: shell |
| 435 | logical, intent(in) :: is_input |
| 436 | character(len=MAX_PATH_LEN) :: fifo_path |
| 437 | logical :: success |
| 438 | integer :: idx |
| 439 | |
| 440 | ! Generate unique FIFO path |
| 441 | fifo_path = generate_fifo_path(shell) |
| 442 | |
| 443 | ! Create the FIFO with mode 0600 (owner read/write) |
| 444 | success = create_fifo(trim(fifo_path)) |
| 445 | |
| 446 | if (.not. success) then |
| 447 | write(error_unit, '(A)') 'fortsh: failed to create FIFO: ' // trim(fifo_path) |
| 448 | fifo_path = '' |
| 449 | return |
| 450 | end if |
| 451 | |
| 452 | ! Track the FIFO in shell state |
| 453 | if (shell%num_proc_subst_fifos < 10) then |
| 454 | idx = shell%num_proc_subst_fifos + 1 |
| 455 | shell%proc_subst_fifos(idx)%fifo_path = fifo_path |
| 456 | shell%proc_subst_fifos(idx)%is_input = is_input |
| 457 | shell%proc_subst_fifos(idx)%active = .true. |
| 458 | shell%proc_subst_fifos(idx)%pid = 0 ! Will be set when process is forked |
| 459 | shell%num_proc_subst_fifos = idx |
| 460 | else |
| 461 | write(error_unit, '(A)') 'fortsh: too many process substitutions (max 10)' |
| 462 | end if |
| 463 | end function |
| 464 | |
| 465 | ! Update FIFO with background process PID |
| 466 | subroutine set_fifo_pid(shell, fifo_path, pid) |
| 467 | type(shell_state_t), intent(inout) :: shell |
| 468 | character(len=*), intent(in) :: fifo_path |
| 469 | integer(c_pid_t), intent(in) :: pid |
| 470 | integer :: i |
| 471 | |
| 472 | do i = 1, shell%num_proc_subst_fifos |
| 473 | if (shell%proc_subst_fifos(i)%active .and. & |
| 474 | trim(shell%proc_subst_fifos(i)%fifo_path) == trim(fifo_path)) then |
| 475 | shell%proc_subst_fifos(i)%pid = pid |
| 476 | return |
| 477 | end if |
| 478 | end do |
| 479 | end subroutine |
| 480 | |
| 481 | ! Clean up a specific FIFO |
| 482 | subroutine cleanup_fifo(shell, fifo_path) |
| 483 | type(shell_state_t), intent(inout) :: shell |
| 484 | character(len=*), intent(in) :: fifo_path |
| 485 | integer :: i |
| 486 | logical :: success |
| 487 | |
| 488 | do i = 1, shell%num_proc_subst_fifos |
| 489 | if (shell%proc_subst_fifos(i)%active .and. & |
| 490 | trim(shell%proc_subst_fifos(i)%fifo_path) == trim(fifo_path)) then |
| 491 | |
| 492 | ! Remove the FIFO file |
| 493 | success = remove_file(trim(fifo_path)) |
| 494 | if (.not. success) then |
| 495 | write(error_unit, '(A)') 'fortsh: warning: failed to remove FIFO: ' // trim(fifo_path) |
| 496 | end if |
| 497 | |
| 498 | ! Mark as inactive |
| 499 | shell%proc_subst_fifos(i)%active = .false. |
| 500 | shell%proc_subst_fifos(i)%fifo_path = '' |
| 501 | shell%proc_subst_fifos(i)%pid = 0 |
| 502 | return |
| 503 | end if |
| 504 | end do |
| 505 | end subroutine |
| 506 | |
| 507 | ! Clean up all active FIFOs |
| 508 | subroutine cleanup_all_fifos(shell) |
| 509 | type(shell_state_t), intent(inout) :: shell |
| 510 | integer :: i |
| 511 | logical :: success |
| 512 | |
| 513 | do i = 1, shell%num_proc_subst_fifos |
| 514 | if (shell%proc_subst_fifos(i)%active) then |
| 515 | success = remove_file(trim(shell%proc_subst_fifos(i)%fifo_path)) |
| 516 | if (.not. success) then |
| 517 | write(error_unit, '(A)') 'fortsh: warning: failed to remove FIFO: ' // & |
| 518 | trim(shell%proc_subst_fifos(i)%fifo_path) |
| 519 | end if |
| 520 | shell%proc_subst_fifos(i)%active = .false. |
| 521 | end if |
| 522 | end do |
| 523 | |
| 524 | shell%num_proc_subst_fifos = 0 |
| 525 | end subroutine |
| 526 | |
| 527 | end module substitution |