Fortran · 16706 bytes Raw Blame History
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