Fortran · 155928 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: expansion
3 ! Purpose: Parameter expansion and arithmetic operations
4 ! ==============================================================================
5 module expansion
6 use shell_types
7 use variables ! includes check_nounset
8 use command_capture, only: execute_command_and_capture
9 use glob, only: pattern_matches_no_dotfile_check
10 use iso_fortran_env, only: output_unit, error_unit
11 #ifdef USE_C_STRINGS
12 use iso_c_binding, only: c_char, c_int, c_null_char, c_ptr, c_f_pointer, c_size_t
13 #endif
14 implicit none
15
16 ! Recursion depth limits
17 integer, parameter :: MAX_RECURSION_DEPTH = 1000
18
19 ! Arithmetic error tracking
20 logical :: arithmetic_error = .false.
21 character(len=256) :: arithmetic_error_msg = ''
22
23 #ifdef USE_C_STRINGS
24 interface
25 function c_pattern_replace_alloc(input, input_len, pattern, pat_len, &
26 replacement, repl_len, replace_all, &
27 result_out) result(out_len) bind(C, name='fortsh_pattern_replace_alloc')
28 import :: c_char, c_int, c_ptr
29 character(kind=c_char), intent(in) :: input(*), pattern(*), replacement(*)
30 integer(c_int), value :: input_len, pat_len, repl_len, replace_all
31 type(c_ptr), intent(out) :: result_out
32 integer(c_int) :: out_len
33 end function
34
35 subroutine c_free_string(ptr) bind(C, name='fortsh_free_string')
36 import :: c_ptr
37 type(c_ptr), value :: ptr
38 end subroutine
39
40 function c_buf_create(capacity) result(handle) bind(C, name='fortsh_buffer_create')
41 import :: c_ptr, c_size_t
42 integer(c_size_t), value :: capacity
43 type(c_ptr) :: handle
44 end function
45
46 subroutine c_buf_destroy(handle) bind(C, name='fortsh_buffer_destroy')
47 import :: c_ptr
48 type(c_ptr), value :: handle
49 end subroutine
50
51 function c_buf_append_chars(handle, str, slen) result(rc) bind(C, name='fortsh_buffer_append_chars')
52 import :: c_ptr, c_int, c_char, c_size_t
53 type(c_ptr), value :: handle
54 character(kind=c_char), intent(in) :: str(*)
55 integer(c_size_t), value :: slen
56 integer(c_int) :: rc
57 end function
58
59 function c_buf_append_char(handle, ch) result(rc) bind(C, name='fortsh_buffer_append_char')
60 import :: c_ptr, c_int, c_char
61 type(c_ptr), value :: handle
62 character(kind=c_char), value :: ch
63 integer(c_int) :: rc
64 end function
65
66 function c_buf_length(handle) result(blen) bind(C, name='fortsh_buffer_length')
67 import :: c_ptr, c_size_t
68 type(c_ptr), value :: handle
69 integer(c_size_t) :: blen
70 end function
71
72 function c_buf_to_fortran(handle, fstr, flen) result(copied) bind(C, name='fortsh_buffer_to_fortran')
73 import :: c_ptr, c_size_t, c_char
74 type(c_ptr), value :: handle
75 character(kind=c_char) :: fstr(*)
76 integer(c_size_t), value :: flen
77 integer(c_size_t) :: copied
78 end function
79
80 subroutine c_buf_clear(handle) bind(C, name='fortsh_buffer_clear')
81 import :: c_ptr
82 type(c_ptr), value :: handle
83 end subroutine
84
85 function c_buf_pattern_replace(input_buf, pattern, pat_len, replacement, repl_len, &
86 replace_all, result_out) result(out_len) &
87 bind(C, name='fortsh_buffer_pattern_replace')
88 import :: c_ptr, c_int, c_char
89 type(c_ptr), value :: input_buf
90 character(kind=c_char), intent(in) :: pattern(*), replacement(*)
91 integer(c_int), value :: pat_len, repl_len, replace_all
92 type(c_ptr), intent(out) :: result_out
93 integer(c_int) :: out_len
94 end function
95 end interface
96 #endif
97
98 contains
99
100 ! Parameter expansion: ${var:offset:length}
101 ! parameter_expansion — wrapper that strips ${ } and delegates to process_param_expansion
102 function parameter_expansion(shell, expression) result(expanded)
103 type(shell_state_t), intent(inout) :: shell
104 character(len=*), intent(in) :: expression
105 character(len=:), allocatable :: expanded
106
107 expanded = ''
108 if (len_trim(expression) < 4) return
109 call process_param_expansion(expression(3:len_trim(expression)-1), expanded, shell)
110 end function
111
112 ! process_param_expansion — canonical parameter expansion implementation
113 ! Takes the content between ${ and }, e.g. for ${var:-default} receives "var:-default"
114 ! This is the subroutine that parser.f90 will delegate to after consolidation.
115 recursive subroutine process_param_expansion(param_expr, result_value, shell)
116 character(len=*), intent(in) :: param_expr
117 character(len=:), allocatable, intent(out) :: result_value
118 type(shell_state_t), intent(inout) :: shell
119
120 character(len=256) :: var_name, operation, param1, param2, replacement
121 character(len=32) :: num_buf
122 character(len=:), allocatable :: pattern
123 character(len=:), allocatable :: var_value
124 integer :: colon_pos, dash_pos, plus_pos, percent_pos, hash_pos, slash_pos, equals_pos, question_pos
125 integer :: offset, length, i, at_pos
126 character :: transform_op
127 logical :: replace_all, greedy, has_colon, var_is_set, var_is_null
128
129 ! Array expansion variables
130 integer :: bracket_pos, bracket_end, j, num_keys
131 character(len=256) :: array_name, array_key
132 character(len=256), allocatable :: keys(:)
133 logical :: is_keys_expansion, is_length_expansion, is_all_expansion
134
135 result_value = ''
136 var_name = param_expr
137
138 ! ========================================================================
139 ! Check for array bracket syntax FIRST: ${array[key]}, ${!array[@]}, ${#array[@]}
140 ! ========================================================================
141 bracket_pos = index(var_name, '[')
142 if (bracket_pos > 0) then
143 bracket_end = index(var_name, ']')
144 if (bracket_end > bracket_pos) then
145 ! Extract array name and key
146 array_name = var_name(:bracket_pos-1)
147 array_key = var_name(bracket_pos+1:bracket_end-1)
148
149 ! Strip quotes from array subscript (bash strips quotes)
150 call strip_quotes(array_key)
151
152 ! Check for special prefixes (! for keys, # for length)
153 is_keys_expansion = .false.
154 is_length_expansion = .false.
155
156 if (len_trim(array_name) > 0 .and. array_name(1:1) == '!') then
157 ! ${!array[@]} - get all keys
158 is_keys_expansion = .true.
159 array_name = array_name(2:) ! Remove ! prefix
160 else if (len_trim(array_name) > 0 .and. array_name(1:1) == '#') then
161 ! ${#array[@]} - get array length
162 is_length_expansion = .true.
163 array_name = array_name(2:) ! Remove # prefix
164 end if
165
166 ! Check for [@] or [*] (all values/keys)
167 is_all_expansion = (trim(array_key) == '@' .or. trim(array_key) == '*')
168
169 ! Handle associative arrays
170 if (is_associative_array(shell, trim(array_name))) then
171 if (is_keys_expansion .and. is_all_expansion) then
172 ! ${!array[@]} - return all keys
173 block
174 character(len=4096) :: kbuf
175 integer :: kpos, klen
176 kbuf = ''; kpos = 1
177 allocate(keys(500))
178 call get_assoc_array_keys(shell, trim(array_name), keys, num_keys)
179 do j = 1, min(num_keys, 500)
180 klen = len_trim(keys(j))
181 if (klen == 0) cycle
182 if (kpos > 1) then; kbuf(kpos:kpos) = ' '; kpos = kpos + 1; end if
183 kbuf(kpos:kpos+klen-1) = keys(j)(1:klen)
184 kpos = kpos + klen
185 end do
186 if (kpos > 1) then
187 result_value = kbuf(1:kpos-1)
188 else
189 result_value = ''
190 end if
191 end block
192 deallocate(keys)
193 return
194 else if (is_length_expansion .and. is_all_expansion) then
195 ! ${#array[@]} - return number of keys
196 allocate(keys(500))
197 call get_assoc_array_keys(shell, trim(array_name), keys, num_keys)
198 deallocate(keys)
199 write(num_buf, '(I0)') num_keys
200 result_value = trim(num_buf)
201 return
202 else if (is_all_expansion) then
203 ! ${array[@]} - return all values
204 block
205 character(len=4096) :: vbuf
206 integer :: vpos, vlen
207 vbuf = ''; vpos = 1
208 allocate(keys(500))
209 call get_assoc_array_keys(shell, trim(array_name), keys, num_keys)
210 do j = 1, min(num_keys, 500)
211 var_value = get_assoc_array_value(shell, trim(array_name), trim(keys(j)))
212 vlen = len_trim(var_value)
213 if (vpos > 1) then; vbuf(vpos:vpos) = ' '; vpos = vpos + 1; end if
214 if (vlen > 0) then; vbuf(vpos:vpos+vlen-1) = var_value(1:vlen); vpos = vpos + vlen; end if
215 end do
216 if (vpos > 1) then
217 result_value = vbuf(1:vpos-1)
218 else
219 result_value = ''
220 end if
221 end block
222 deallocate(keys)
223 return
224 else
225 ! ${array[key]} - get value for specific key
226 var_value = get_assoc_array_value(shell, trim(array_name), trim(array_key))
227 result_value = trim(var_value)
228 return
229 end if
230 else
231 ! Handle indexed arrays
232 if (is_all_expansion) then
233 ! ${arr[@]} or ${arr[*]} — all elements
234 if (is_keys_expansion) then
235 ! ${!arr[@]} — list indices of set elements
236 block
237 integer :: ki, arr_count
238 character(len=4096) :: all_str
239 all_str = trim(get_array_all_elements(shell, trim(array_name)))
240 ! Count space-separated words to get indices
241 arr_count = 0
242 result_value = ''
243 do ki = 1, len_trim(all_str)
244 if (all_str(ki:ki) == ' ') arr_count = arr_count + 1
245 end do
246 if (len_trim(all_str) > 0) arr_count = arr_count + 1
247 ! Generate 0-based indices
248 do ki = 0, arr_count - 1
249 if (ki > 0) result_value = result_value // ' '
250 write(num_buf, '(I0)') ki
251 result_value = result_value // trim(num_buf)
252 end do
253 end block
254 return
255 else if (is_length_expansion) then
256 ! ${#arr[@]} — count of elements
257 block
258 integer :: ki, arr_count
259 character(len=4096) :: all_str
260 all_str = trim(get_array_all_elements(shell, trim(array_name)))
261 arr_count = 0
262 if (len_trim(all_str) > 0) then
263 arr_count = 1
264 do ki = 1, len_trim(all_str)
265 if (all_str(ki:ki) == ' ') arr_count = arr_count + 1
266 end do
267 end if
268 write(num_buf, '(I0)') arr_count
269 result_value = trim(num_buf)
270 end block
271 return
272 else
273 ! ${arr[@]} — all values, with optional slicing
274 block
275 character(len=4096) :: all_str
276 character(len=256) :: slice_spec
277 integer :: colon_after, sc_pos2, ios1, ios2
278 integer :: s_offset, s_length, w_count, w_start, w_idx, w_out
279 logical :: has_slice
280 all_str = trim(get_array_all_elements(shell, trim(array_name)))
281
282 ! Check for slice syntax after ]: ${arr[@]:offset:length}
283 has_slice = .false.
284 if (bracket_end < len_trim(var_name)) then
285 slice_spec = var_name(bracket_end+1:)
286 if (len_trim(slice_spec) > 0 .and. slice_spec(1:1) == ':') then
287 has_slice = .true.
288 end if
289 end if
290
291 if (.not. has_slice) then
292 result_value = trim(all_str)
293 return
294 end if
295
296 ! Parse :offset or :offset:length
297 slice_spec = slice_spec(2:) ! skip leading :
298 sc_pos2 = index(trim(slice_spec), ':')
299 if (sc_pos2 > 0) then
300 read(slice_spec(:sc_pos2-1), *, iostat=ios1) s_offset
301 read(slice_spec(sc_pos2+1:), *, iostat=ios2) s_length
302 if (ios2 /= 0) s_length = 9999
303 else
304 read(slice_spec, *, iostat=ios1) s_offset
305 s_length = 9999
306 end if
307 if (ios1 /= 0) then
308 result_value = trim(all_str)
309 return
310 end if
311
312 ! Split into words and select slice
313 result_value = ''
314 w_count = 0; w_start = 1; w_out = 0
315 do w_idx = 1, len_trim(all_str) + 1
316 if (w_idx > len_trim(all_str) .or. all_str(w_idx:w_idx) == ' ') then
317 if (w_idx > w_start) then
318 if (w_count >= s_offset .and. w_out < s_length) then
319 if (w_out > 0) result_value = result_value // ' '
320 result_value = result_value // all_str(w_start:w_idx-1)
321 w_out = w_out + 1
322 end if
323 w_count = w_count + 1
324 end if
325 w_start = w_idx + 1
326 end if
327 end do
328 end block
329 return
330 end if
331 else
332 ! ${arr[i]} — single element (supports negative indices)
333 block
334 integer :: arr_idx, arr_ios, arr_size
335 character(len=:), allocatable :: arr_val
336 read(array_key, *, iostat=arr_ios) arr_idx
337 if (arr_ios == 0) then
338 if (arr_idx < 0) then
339 arr_size = get_array_size(shell, trim(array_name))
340 arr_idx = arr_size + arr_idx ! -1 → last element (0-based)
341 end if
342 arr_val = get_array_element(shell, trim(array_name), arr_idx + 1)
343 if (is_length_expansion) then
344 write(num_buf, '(I0)') len_trim(arr_val)
345 result_value = trim(num_buf)
346 else
347 result_value = trim(arr_val)
348 end if
349 return
350 end if
351 end block
352 end if
353 end if
354 end if
355 end if
356
357 ! ========================================================================
358 ! Handle indirect expansion prefix: ${!ref...}
359 ! Resolve !name to the variable name it references, then continue with
360 ! normal operator handling so ${!ref:-default} works correctly.
361 ! NOTE: Only one level of indirection is resolved (no recursion).
362 ! Circular references (x->y->x) safely stop after one resolution.
363 ! ========================================================================
364 if (len_trim(var_name) > 1 .and. var_name(1:1) == '!' .and. index(var_name, '[') == 0) then
365 block
366 integer :: ref_end
367 character(len=4096) :: ref_name, resolved_name
368 ! Extract reference variable name (alphanumeric/underscore chars after !)
369 ref_end = 2
370 do while (ref_end <= len_trim(var_name))
371 if (.not. (var_name(ref_end:ref_end) >= 'a' .and. var_name(ref_end:ref_end) <= 'z') .and. &
372 .not. (var_name(ref_end:ref_end) >= 'A' .and. var_name(ref_end:ref_end) <= 'Z') .and. &
373 .not. (var_name(ref_end:ref_end) >= '0' .and. var_name(ref_end:ref_end) <= '9') .and. &
374 var_name(ref_end:ref_end) /= '_') exit
375 ref_end = ref_end + 1
376 end do
377 ref_name = var_name(2:ref_end-1)
378 resolved_name = get_shell_variable(shell, trim(ref_name))
379 if (len_trim(resolved_name) > 0) then
380 ! Replace !ref with resolved name, keep any trailing operators
381 if (ref_end <= len_trim(var_name)) then
382 var_name = trim(resolved_name) // var_name(ref_end:len_trim(var_name))
383 else
384 var_name = trim(resolved_name)
385 end if
386 else
387 ! Reference variable is unset — error + empty (matches bash)
388 call write_stderr('fortsh: ' // trim(ref_name) // ': invalid indirect expansion')
389 result_value = ''
390 return
391 end if
392 end block
393 end if
394
395 ! Check for various expansion operations (need to check in right order!)
396
397 ! Check for @ transformations first: ${var@U}, ${var@L}, ${var@u}, ${var@Q}, ${var@E}
398 at_pos = index(var_name, '@')
399 if (at_pos > 0 .and. at_pos < len_trim(var_name)) then
400 ! Extract variable name and transformation operator
401 operation = var_name(:at_pos-1)
402 transform_op = var_name(at_pos+1:at_pos+1)
403 var_value = get_shell_variable(shell, trim(operation))
404
405 select case (transform_op)
406 case ('U')
407 ! ${var@U} - convert to uppercase
408 result_value = to_upper(trim(var_value))
409 return
410 case ('L')
411 ! ${var@L} - convert to lowercase
412 result_value = to_lower(trim(var_value))
413 return
414 case ('u')
415 ! ${var@u} - capitalize first character
416 if (len_trim(var_value) > 0) then
417 result_value = to_upper(var_value(1:1))
418 if (len_trim(var_value) > 1) result_value = trim(result_value) // var_value(2:)
419 end if
420 return
421 case ('l')
422 ! ${var@l} - lowercase first character
423 if (len_trim(var_value) > 0) then
424 result_value = to_lower(var_value(1:1))
425 if (len_trim(var_value) > 1) result_value = trim(result_value) // var_value(2:)
426 end if
427 return
428 case ('Q')
429 ! ${var@Q} - shell-quote value (wrap in single quotes, escape embedded quotes)
430 result_value = quote_value(trim(var_value))
431 return
432 case ('E')
433 ! ${var@E} - expand escape sequences
434 result_value = expand_escape_sequences(trim(var_value))
435 return
436 end select
437 end if
438
439 ! Check for case conversion first (^, ^^, ,, ,,)
440 ! Find the position of ^ or , to determine if it's case conversion
441 i = len_trim(var_name)
442 if (i > 1) then
443 if (var_name(i:i) == '^') then
444 ! Check for ^^ (uppercase all) or ^ (uppercase first)
445 if (i > 1 .and. var_name(i-1:i-1) == '^') then
446 ! ${var^^} - uppercase all
447 operation = var_name(:i-2)
448 var_value = get_shell_variable(shell, trim(operation))
449 result_value = to_upper(trim(var_value))
450 else
451 ! ${var^} - uppercase first
452 operation = var_name(:i-1)
453 var_value = get_shell_variable(shell, trim(operation))
454 if (len_trim(var_value) > 0) then
455 result_value = to_upper(var_value(1:1))
456 if (len_trim(var_value) > 1) result_value = trim(result_value) // var_value(2:)
457 end if
458 end if
459 return
460 else if (var_name(i:i) == ',') then
461 ! Check for ,, (lowercase all) or , (lowercase first)
462 if (i > 1 .and. var_name(i-1:i-1) == ',') then
463 ! ${var,,} - lowercase all
464 operation = var_name(:i-2)
465 var_value = get_shell_variable(shell, trim(operation))
466 result_value = to_lower(trim(var_value))
467 else
468 ! ${var,} - lowercase first
469 operation = var_name(:i-1)
470 var_value = get_shell_variable(shell, trim(operation))
471 if (len_trim(var_value) > 0) then
472 result_value = to_lower(var_value(1:1))
473 if (len_trim(var_value) > 1) result_value = trim(result_value) // var_value(2:)
474 end if
475 end if
476 return
477 end if
478 end if
479
480 ! Find operator positions, skipping characters inside nested ${...}
481 colon_pos = 0; dash_pos = 0; plus_pos = 0; equals_pos = 0
482 question_pos = 0; percent_pos = 0; hash_pos = 0; slash_pos = 0
483 block
484 integer :: scan_i, scan_depth
485 scan_depth = 0
486 do scan_i = 1, len_trim(var_name)
487 ! Track brace nesting: skip everything inside ${...}
488 if (scan_i + 1 <= len_trim(var_name) .and. &
489 var_name(scan_i:scan_i) == '$' .and. var_name(scan_i+1:scan_i+1) == '{') then
490 scan_depth = scan_depth + 1
491 else if (var_name(scan_i:scan_i) == '}' .and. scan_depth > 0) then
492 scan_depth = scan_depth - 1
493 end if
494 if (scan_depth > 0) cycle ! inside nested ${...}, skip
495
496 ! Record first occurrence of each operator
497 select case (var_name(scan_i:scan_i))
498 case (':')
499 if (colon_pos == 0) colon_pos = scan_i
500 case ('-')
501 if (dash_pos == 0) dash_pos = scan_i
502 case ('+')
503 if (plus_pos == 0) plus_pos = scan_i
504 case ('=')
505 if (equals_pos == 0) equals_pos = scan_i
506 case ('?')
507 if (question_pos == 0) question_pos = scan_i
508 case ('%')
509 if (percent_pos == 0) percent_pos = scan_i
510 case ('#')
511 if (hash_pos == 0) hash_pos = scan_i
512 case ('/')
513 if (slash_pos == 0) slash_pos = scan_i
514 end select
515 end do
516 end block
517 ! write(error_unit, '(A,A,A,I0)') 'DEBUG AFTER OPS: var_name=[', trim(var_name), '] dash_pos=', dash_pos
518
519 ! Pattern replacement: ${var/pattern/replacement} or ${var//pattern/replacement}
520 ! Only if / appears before # and % (otherwise /*/ in ${var#/*/} is a pattern, not replacement)
521 if (slash_pos > 0 .and. &
522 (hash_pos == 0 .or. slash_pos < hash_pos) .and. &
523 (percent_pos == 0 .or. slash_pos < percent_pos)) then
524 ! Check if it's replace all (//) — look AFTER slash_pos for second /
525 operation = var_name(:slash_pos-1)
526 if (slash_pos < len_trim(var_name) .and. var_name(slash_pos+1:slash_pos+1) == '/') then
527 replace_all = .true.
528 ! Skip both slashes: pattern starts after //
529 i = index(var_name(slash_pos+2:), '/')
530 if (i > 0) then
531 i = slash_pos + 1 + i
532 pattern = var_name(slash_pos+2:i-1)
533 replacement = var_name(i+1:)
534 else
535 pattern = var_name(slash_pos+2:)
536 replacement = ''
537 end if
538 else
539 replace_all = .false.
540 ! Single /: find separator slash after pattern
541 i = index(var_name(slash_pos+1:), '/')
542 if (i > 0) then
543 i = slash_pos + i
544 pattern = var_name(slash_pos+1:i-1)
545 replacement = var_name(i+1:)
546 else
547 pattern = var_name(slash_pos+1:)
548 replacement = ''
549 end if
550 end if
551
552 var_value = get_shell_variable(shell, trim(operation))
553
554 ! Check for anchor prefix in pattern
555 if (len_trim(pattern) > 0 .and. pattern(1:1) == '#') then
556 ! Anchored at start: ${var/#pat/repl}
557 pattern = pattern(2:)
558 if (len_trim(pattern) == 0) then
559 result_value = trim(replacement) // trim(var_value)
560 else if (len_trim(var_value) >= len_trim(pattern) .and. &
561 var_value(1:len_trim(pattern)) == trim(pattern)) then
562 result_value = trim(replacement) // var_value(len_trim(pattern)+1:len_trim(var_value))
563 else
564 result_value = var_value
565 end if
566 else if (len_trim(pattern) > 0 .and. pattern(1:1) == '%') then
567 ! Anchored at end: ${var/%pat/repl}
568 pattern = pattern(2:)
569 if (len_trim(pattern) == 0) then
570 result_value = trim(var_value) // trim(replacement)
571 else if (len_trim(var_value) >= len_trim(pattern) .and. &
572 var_value(len_trim(var_value)-len_trim(pattern)+1:len_trim(var_value)) == &
573 trim(pattern)) then
574 result_value = var_value(1:len_trim(var_value)-len_trim(pattern)) // trim(replacement)
575 else
576 result_value = var_value
577 end if
578 else
579 call pattern_replace(trim(var_value), trim(pattern), trim(replacement), &
580 replace_all, result_value)
581 end if
582 return
583 end if
584
585 ! Suffix removal: ${var%pattern} or ${var%%pattern}
586 if (percent_pos > 0) then
587 ! Check for %%
588 if (percent_pos < len_trim(var_name) .and. var_name(percent_pos+1:percent_pos+1) == '%') then
589 ! ${var%%pattern} - remove largest matching suffix
590 greedy = .true.
591 operation = var_name(:percent_pos-1)
592 pattern = var_name(percent_pos+2:)
593 else
594 ! ${var%pattern} - remove smallest matching suffix
595 greedy = .false.
596 operation = var_name(:percent_pos-1)
597 pattern = var_name(percent_pos+1:)
598 end if
599 var_value = get_shell_variable(shell, trim(operation))
600 ! Expand $var or ${...} in pattern
601 if (index(trim(pattern), '$') > 0) then
602 pattern = expand_word_operand(trim(pattern), shell)
603 end if
604 call remove_suffix(trim(var_value), trim(pattern), greedy, result_value)
605 return
606 end if
607
608 ! Prefix removal: ${var#pattern} or ${var##pattern}
609 ! But first check if it's ${#var} (length)
610 if (hash_pos == 1) then
611 ! Check if this is just ${#} (number of positional params)
612 if (len_trim(var_name) == 1) then
613 ! ${#} alone - return number of positional parameters
614 write(num_buf, '(I0)') shell%num_positional
615 result_value = trim(num_buf)
616 return
617 else if (len_trim(var_name) > 1) then
618 ! ${#var} length expansion
619 operation = var_name(2:)
620
621 ! Check for special parameters
622 if (trim(operation) == '@' .or. trim(operation) == '*') then
623 ! ${#@} or ${#*} - return number of positional parameters
624 write(num_buf, '(I0)') shell%num_positional
625 result_value = trim(num_buf)
626 return
627 else if (len(trim(operation)) > 0) then
628 ! Check if it's a positional parameter (digit)
629 read(operation, *, iostat=i) j
630 if (i == 0 .and. j > 0) then
631 ! ${#1}, ${#2}, etc. - return length of specific positional parameter
632 if (j <= shell%num_positional) then
633 write(num_buf, '(I0)') len_trim(shell%positional_params(j)%str)
634 result_value = trim(num_buf)
635 else
636 result_value = '0'
637 end if
638 return
639 else
640 ! Regular variable length
641 var_value = get_shell_variable(shell, trim(operation))
642 write(num_buf, '(I0)') len_trim(var_value)
643 result_value = trim(num_buf)
644 return
645 end if
646 end if
647 end if
648 else if (hash_pos > 1) then
649 ! Check for ##
650 if (hash_pos < len_trim(var_name) .and. var_name(hash_pos+1:hash_pos+1) == '#') then
651 ! ${var##pattern} - remove largest matching prefix
652 greedy = .true.
653 operation = var_name(:hash_pos-1)
654 pattern = var_name(hash_pos+2:)
655 else
656 ! ${var#pattern} - remove smallest matching prefix
657 greedy = .false.
658 operation = var_name(:hash_pos-1)
659 pattern = var_name(hash_pos+1:)
660 end if
661 var_value = get_shell_variable(shell, trim(operation))
662 ! Expand $var or ${...} in pattern
663 if (index(trim(pattern), '$') > 0) then
664 pattern = expand_word_operand(trim(pattern), shell)
665 end if
666 call remove_prefix(trim(var_value), trim(pattern), greedy, result_value)
667 return
668 end if
669
670 ! Check if colon is for substring expansion (followed by digit) or parameter expansion (followed by operator)
671 if (colon_pos > 0) then
672 ! Check what follows the colon
673 if (colon_pos < len_trim(var_name)) then
674 ! If followed by an operator (-+?=), it's parameter expansion, not substring
675 if (var_name(colon_pos+1:colon_pos+1) == '-' .or. &
676 var_name(colon_pos+1:colon_pos+1) == '+' .or. &
677 var_name(colon_pos+1:colon_pos+1) == '?' .or. &
678 var_name(colon_pos+1:colon_pos+1) == '=') then
679 ! This is parameter expansion like ${var:-word}, handle it below
680 else
681 ! This is substring expansion ${var:offset:length}
682 call parse_substring_expansion(var_name, operation, param1, param2)
683 var_value = get_shell_variable(shell, trim(operation))
684
685 if (len_trim(param1) > 0) then
686 read(param1, *, iostat=i) offset
687 if (i /= 0) offset = 0
688 ! Handle negative offsets (count from end)
689 if (offset < 0) offset = len_trim(var_value) + offset
690 if (offset < 0) offset = 0
691 if (len_trim(param2) > 0) then
692 read(param2, *, iostat=i) length
693 if (i /= 0) length = 0
694 if (offset < len_trim(var_value)) then
695 i = min(length, len_trim(var_value) - offset)
696 result_value = var_value(offset+1:offset+i)
697 end if
698 else
699 if (offset < len_trim(var_value)) then
700 result_value = var_value(offset+1:len_trim(var_value))
701 end if
702 end if
703 else
704 result_value = var_value
705 end if
706 return
707 end if
708 end if
709 end if
710
711 if (dash_pos > 0 .or. plus_pos > 0 .or. equals_pos > 0 .or. question_pos > 0) then
712 ! ${var-word}, ${var:-word}, ${var+word}, ${var:+word}, ${var=word}, ${var:=word}, ${var?word}, ${var:?word}
713 ! write(error_unit, '(A,I0,A,I0,A,I0,A,I0)') 'DEBUG: dash=', dash_pos, &
714 ! ' plus=', plus_pos, ' eq=', equals_pos, ' q=', question_pos
715
716 ! Determine which operator we have
717 if (dash_pos > 0 .and. (plus_pos == 0 .or. dash_pos < plus_pos) .and. &
718 (equals_pos == 0 .or. dash_pos < equals_pos) .and. (question_pos == 0 .or. dash_pos < question_pos)) then
719 ! Dash operator
720 ! write(error_unit, '(A)') 'DEBUG: Entering dash operator handler'
721 has_colon = (dash_pos > 1 .and. var_name(dash_pos-1:dash_pos-1) == ':')
722 if (has_colon) then
723 operation = var_name(:dash_pos-2)
724 param1 = var_name(dash_pos+1:)
725 else
726 operation = var_name(:dash_pos-1)
727 param1 = var_name(dash_pos+1:)
728 end if
729
730 ! write(error_unit, '(A,L1,A,A,A,A,A)') 'DEBUG: has_colon=', has_colon, &
731 ! ' op=', trim(operation), ' param1=', trim(param1)
732 var_is_set = is_shell_variable_set(shell, trim(operation))
733 var_value = get_shell_variable(shell, trim(operation))
734 var_is_null = (len_trim(var_value) == 0)
735 ! write(error_unit, '(A,L1,A,A,A,L1)') 'DEBUG: var_is_set=', var_is_set, &
736 ! ' val=', trim(var_value), ' null=', var_is_null
737
738 ! ${var-word}: use word if var is unset
739 ! ${var:-word}: use word if var is unset or null
740 if (has_colon) then
741 if (.not. var_is_set .or. var_is_null) then
742 result_value = expand_word_operand(trim(param1), shell)
743 else
744 result_value = trim(var_value)
745 end if
746 else
747 if (.not. var_is_set) then
748 result_value = expand_word_operand(trim(param1), shell)
749 else
750 result_value = trim(var_value)
751 end if
752 end if
753 ! write(error_unit, '(A,A,A)') 'DEBUG: result_value=', trim(result_value), '|'
754
755 else if (plus_pos > 0 .and. (equals_pos == 0 .or. plus_pos < equals_pos) .and. &
756 (question_pos == 0 .or. plus_pos < question_pos)) then
757 ! Plus operator
758 has_colon = (plus_pos > 1 .and. var_name(plus_pos-1:plus_pos-1) == ':')
759 if (has_colon) then
760 operation = var_name(:plus_pos-2)
761 param1 = var_name(plus_pos+1:)
762 else
763 operation = var_name(:plus_pos-1)
764 param1 = var_name(plus_pos+1:)
765 end if
766
767 var_is_set = is_shell_variable_set(shell, trim(operation))
768 var_value = get_shell_variable(shell, trim(operation))
769 var_is_null = (len_trim(var_value) == 0)
770
771 ! ${var+word}: use word if var is set (even if null)
772 ! ${var:+word}: use word if var is set and not null
773 if (has_colon) then
774 if (var_is_set .and. .not. var_is_null) then
775 result_value = expand_word_operand(trim(param1), shell)
776 else
777 result_value = ''
778 end if
779 else
780 if (var_is_set) then
781 result_value = expand_word_operand(trim(param1), shell)
782 else
783 result_value = ''
784 end if
785 end if
786
787 else if (equals_pos > 0 .and. (question_pos == 0 .or. equals_pos < question_pos)) then
788 ! Equals operator (assign and expand)
789 has_colon = (equals_pos > 1 .and. var_name(equals_pos-1:equals_pos-1) == ':')
790 if (has_colon) then
791 operation = var_name(:equals_pos-2)
792 param1 = var_name(equals_pos+1:)
793 else
794 operation = var_name(:equals_pos-1)
795 param1 = var_name(equals_pos+1:)
796 end if
797
798 var_is_set = is_shell_variable_set(shell, trim(operation))
799 var_value = get_shell_variable(shell, trim(operation))
800 var_is_null = (len_trim(var_value) == 0)
801
802 ! ${var=word}: assign word if var is unset, then expand to var
803 ! ${var:=word}: assign word if var is unset or null, then expand to var
804 if (has_colon) then
805 if (.not. var_is_set .or. var_is_null) then
806 call set_shell_variable(shell, trim(operation), trim(param1))
807 result_value = expand_word_operand(trim(param1), shell)
808 else
809 result_value = trim(var_value)
810 end if
811 else
812 if (.not. var_is_set) then
813 call set_shell_variable(shell, trim(operation), trim(param1))
814 result_value = expand_word_operand(trim(param1), shell)
815 else
816 result_value = trim(var_value)
817 end if
818 end if
819
820 else if (question_pos > 0) then
821 ! Question operator (error if unset)
822 has_colon = (question_pos > 1 .and. var_name(question_pos-1:question_pos-1) == ':')
823 if (has_colon) then
824 operation = var_name(:question_pos-2)
825 param1 = var_name(question_pos+1:)
826 else
827 operation = var_name(:question_pos-1)
828 param1 = var_name(question_pos+1:)
829 end if
830
831 var_is_set = is_shell_variable_set(shell, trim(operation))
832 var_value = get_shell_variable(shell, trim(operation))
833 var_is_null = (len_trim(var_value) == 0)
834
835 ! ${var?word}: error if var is unset
836 ! ${var:?word}: error if var is unset or null
837 if (has_colon) then
838 if (.not. var_is_set .or. var_is_null) then
839 if (len_trim(param1) > 0) then
840 write(error_unit, '(A)') trim(operation) // ': ' // trim(param1)
841 else
842 write(error_unit, '(A)') trim(operation) // ': parameter null or not set'
843 end if
844 shell%last_exit_status = 127 ! bash uses 127 for direct expansion errors
845 shell%fatal_expansion_error = .true. ! Signal to abort execution
846 result_value = ''
847 else
848 result_value = trim(var_value)
849 end if
850 else
851 if (.not. var_is_set) then
852 if (len_trim(param1) > 0) then
853 write(error_unit, '(A)') trim(operation) // ': ' // trim(param1)
854 else
855 write(error_unit, '(A)') trim(operation) // ': parameter not set'
856 end if
857 shell%last_exit_status = 127 ! bash uses 127 for direct expansion errors
858 shell%fatal_expansion_error = .true. ! Signal to abort execution
859 result_value = ''
860 else
861 result_value = trim(var_value)
862 end if
863 end if
864 end if
865 return
866
867 else if (hash_pos > 0) then
868 ! ${#var} length expansion
869 operation = var_name(hash_pos+1:)
870 var_value = get_shell_variable(shell, trim(operation))
871 write(num_buf, '(I0)') len_trim(var_value)
872 result_value = trim(num_buf)
873
874 else
875 ! Simple variable expansion
876 var_value = get_shell_variable(shell, trim(var_name))
877
878 ! Check if variable is unset and set -u is enabled
879 if (len_trim(var_value) == 0 .and. .not. is_shell_variable_set(shell, trim(var_name))) then
880 if (check_nounset(shell, trim(var_name))) then
881 shell%last_exit_status = 127 ! bash uses 127 for direct expansion errors
882 shell%fatal_expansion_error = .true.
883 result_value = ''
884 return
885 end if
886 end if
887
888 result_value = trim(var_value)
889 end if
890
891 end subroutine process_param_expansion
892
893 ! Expand nested ${...} in a word operand (used by default/assign/error/alternate operators)
894 recursive function expand_word_operand(word, shell) result(expanded)
895 character(len=*), intent(in) :: word
896 type(shell_state_t), intent(inout) :: shell
897 character(len=:), allocatable :: expanded
898 integer :: dp, bp, depth
899
900 ! Quick check: if no $, return as-is
901 dp = index(word, '$')
902 if (dp == 0) then
903 expanded = trim(word)
904 return
905 end if
906
907 ! Handle simple $VAR (no braces)
908 if (dp + 1 <= len_trim(word) .and. word(dp+1:dp+1) /= '{') then
909 block
910 integer :: vend
911 character(len=:), allocatable :: vval
912 vend = dp + 1
913 do while (vend <= len_trim(word))
914 if (.not. ((word(vend:vend) >= 'a' .and. word(vend:vend) <= 'z') .or. &
915 (word(vend:vend) >= 'A' .and. word(vend:vend) <= 'Z') .or. &
916 (word(vend:vend) >= '0' .and. word(vend:vend) <= '9') .or. &
917 word(vend:vend) == '_')) exit
918 vend = vend + 1
919 end do
920 if (vend > dp + 1) then
921 vval = get_shell_variable(shell, word(dp+1:vend-1))
922 expanded = word(1:dp-1) // trim(vval)
923 if (vend <= len_trim(word)) then
924 expanded = expanded // expand_word_operand(word(vend:), shell)
925 end if
926 return
927 end if
928 end block
929 end if
930
931 ! Handle ${...} brace expansion
932 dp = index(word, '${')
933
934 ! Find matching } counting nesting depth — start AFTER the opening ${
935 depth = 1
936 bp = dp + 2
937 do while (bp <= len_trim(word))
938 if (bp + 1 <= len_trim(word) .and. word(bp:bp) == '$' .and. word(bp+1:bp+1) == '{') then
939 depth = depth + 1
940 bp = bp + 2
941 cycle
942 else if (word(bp:bp) == '}') then
943 depth = depth - 1
944 if (depth == 0) exit
945 end if
946 bp = bp + 1
947 end do
948
949 if (bp <= len_trim(word)) then
950 ! Recursively expand the inner ${...}
951 block
952 character(len=:), allocatable :: inner_result, prefix, suffix
953 prefix = word(1:dp-1)
954 call process_param_expansion(word(dp+2:bp-1), inner_result, shell)
955 suffix = word(bp+1:len_trim(word))
956 ! Recurse on suffix in case there are more ${...}
957 if (index(suffix, '${') > 0) then
958 expanded = prefix // inner_result // expand_word_operand(suffix, shell)
959 else
960 expanded = prefix // inner_result // suffix
961 end if
962 end block
963 else
964 expanded = trim(word)
965 end if
966 end function
967
968 subroutine parse_substring_expansion(input, var_name, offset_str, length_str)
969 character(len=*), intent(in) :: input
970 character(len=*), intent(out) :: var_name, offset_str, length_str
971 integer :: first_colon, second_colon
972
973 var_name = ''
974 offset_str = ''
975 length_str = ''
976
977 first_colon = index(input, ':')
978 if (first_colon == 0) return
979
980 var_name = input(:first_colon-1)
981
982 second_colon = index(input(first_colon+1:), ':')
983 if (second_colon > 0) then
984 second_colon = first_colon + second_colon
985 offset_str = input(first_colon+1:second_colon-1)
986 length_str = input(second_colon+1:)
987 else
988 offset_str = input(first_colon+1:)
989 end if
990 end subroutine
991
992 ! ============================================================================
993 ! Parameter Expansion Helper Functions
994 ! ============================================================================
995
996 ! Convert string to uppercase
997 function to_upper(input) result(output)
998 character(len=*), intent(in) :: input
999 character(len=len(input)) :: output
1000 integer :: i, char_code
1001
1002 output = input
1003 do i = 1, len_trim(input)
1004 char_code = ichar(input(i:i))
1005 if (char_code >= ichar('a') .and. char_code <= ichar('z')) then
1006 output(i:i) = char(char_code - 32)
1007 end if
1008 end do
1009 end function
1010
1011 ! Convert string to lowercase
1012 function to_lower(input) result(output)
1013 character(len=*), intent(in) :: input
1014 character(len=len(input)) :: output
1015 integer :: i, char_code
1016
1017 output = input
1018 do i = 1, len_trim(input)
1019 char_code = ichar(input(i:i))
1020 if (char_code >= ichar('A') .and. char_code <= ichar('Z')) then
1021 output(i:i) = char(char_code + 32)
1022 end if
1023 end do
1024 end function
1025
1026 ! Quote value - wrap in single quotes and escape embedded single quotes
1027 ! Used for ${var@Q} transformation
1028 function quote_value(input) result(output)
1029 character(len=*), intent(in) :: input
1030 character(len=:), allocatable :: output
1031 character(len=:), allocatable :: temp_output
1032 integer :: i, out_pos, capacity
1033
1034 if (len_trim(input) == 0) then
1035 output = "''"
1036 return
1037 end if
1038
1039 ! Allocate with initial capacity
1040 capacity = len(input) * 2 + 10
1041 allocate(character(len=capacity) :: temp_output)
1042 temp_output = "'"
1043 out_pos = 2
1044
1045 do i = 1, len_trim(input)
1046 if (input(i:i) == "'") then
1047 ! Ensure we have space for escape sequence
1048 if (out_pos + 4 > capacity) then
1049 call grow_string_buffer_exp(temp_output, capacity, capacity * 2, out_pos - 1)
1050 end if
1051 ! Escape single quote: ' becomes '\''
1052 temp_output(out_pos:out_pos+3) = "'\'''"
1053 out_pos = out_pos + 4
1054 else
1055 if (out_pos > capacity) then
1056 call grow_string_buffer_exp(temp_output, capacity, capacity * 2, out_pos - 1)
1057 end if
1058 temp_output(out_pos:out_pos) = input(i:i)
1059 out_pos = out_pos + 1
1060 end if
1061 end do
1062
1063 if (out_pos > capacity) then
1064 call grow_string_buffer_exp(temp_output, capacity, capacity * 2, out_pos - 1)
1065 end if
1066 temp_output(out_pos:out_pos) = "'"
1067 output = temp_output(1:out_pos)
1068 deallocate(temp_output)
1069 end function
1070
1071 ! Expand escape sequences in string
1072 ! Used for ${var@E} transformation
1073 function expand_escape_sequences(input) result(output)
1074 character(len=*), intent(in) :: input
1075 character(len=:), allocatable :: output
1076 character(len=:), allocatable :: temp_output
1077 integer :: i, out_pos, capacity, esc_hex_val, esc_n_digits
1078 character :: esc_ch
1079
1080 ! Allocate with initial capacity
1081 capacity = len(input) + 100
1082 allocate(character(len=capacity) :: temp_output)
1083 temp_output = ''
1084 out_pos = 1
1085 i = 1
1086
1087 do while (i <= len_trim(input))
1088 if (input(i:i) == '\' .and. i < len_trim(input)) then
1089 ! Escape sequence
1090 i = i + 1
1091 select case (input(i:i))
1092 case ('n')
1093 temp_output(out_pos:out_pos) = char(10) ! newline
1094 out_pos = out_pos + 1
1095 case ('t')
1096 temp_output(out_pos:out_pos) = char(9) ! tab
1097 out_pos = out_pos + 1
1098 case ('r')
1099 temp_output(out_pos:out_pos) = char(13) ! carriage return
1100 out_pos = out_pos + 1
1101 case ('b')
1102 temp_output(out_pos:out_pos) = char(8) ! backspace
1103 out_pos = out_pos + 1
1104 case ('f')
1105 temp_output(out_pos:out_pos) = char(12) ! form feed
1106 out_pos = out_pos + 1
1107 case ('v')
1108 temp_output(out_pos:out_pos) = char(11) ! vertical tab
1109 out_pos = out_pos + 1
1110 case ('a')
1111 temp_output(out_pos:out_pos) = char(7) ! alert/bell
1112 out_pos = out_pos + 1
1113 case ('e', 'E')
1114 temp_output(out_pos:out_pos) = char(27) ! escape
1115 out_pos = out_pos + 1
1116 case ('\')
1117 temp_output(out_pos:out_pos) = '\' ! backslash
1118 out_pos = out_pos + 1
1119 case ('"')
1120 temp_output(out_pos:out_pos) = '"' ! double quote
1121 out_pos = out_pos + 1
1122 case ("'")
1123 temp_output(out_pos:out_pos) = "'" ! single quote
1124 out_pos = out_pos + 1
1125 case ('x')
1126 ! Hex escape: \xHH (up to 2 hex digits)
1127 esc_hex_val = 0; esc_n_digits = 0
1128 i = i + 1 ! skip 'x'
1129 do while (i <= len_trim(input) .and. esc_n_digits < 2)
1130 esc_ch = input(i:i)
1131 if (esc_ch >= '0' .and. esc_ch <= '9') then
1132 esc_hex_val = esc_hex_val * 16 + (ichar(esc_ch) - ichar('0'))
1133 else if (esc_ch >= 'a' .and. esc_ch <= 'f') then
1134 esc_hex_val = esc_hex_val * 16 + (ichar(esc_ch) - ichar('a') + 10)
1135 else if (esc_ch >= 'A' .and. esc_ch <= 'F') then
1136 esc_hex_val = esc_hex_val * 16 + (ichar(esc_ch) - ichar('A') + 10)
1137 else
1138 exit
1139 end if
1140 i = i + 1
1141 esc_n_digits = esc_n_digits + 1
1142 end do
1143 if (esc_n_digits > 0 .and. esc_hex_val <= 255) then
1144 temp_output(out_pos:out_pos) = char(esc_hex_val)
1145 out_pos = out_pos + 1
1146 end if
1147 cycle ! i already advanced past hex digits
1148 case ('0', '1', '2', '3', '4', '5', '6', '7')
1149 ! Octal escape: \nnn (up to 3 octal digits)
1150 esc_hex_val = 0; esc_n_digits = 0
1151 do while (i <= len_trim(input) .and. esc_n_digits < 3)
1152 esc_ch = input(i:i)
1153 if (esc_ch >= '0' .and. esc_ch <= '7') then
1154 esc_hex_val = esc_hex_val * 8 + (ichar(esc_ch) - ichar('0'))
1155 else
1156 exit
1157 end if
1158 i = i + 1
1159 esc_n_digits = esc_n_digits + 1
1160 end do
1161 if (esc_n_digits > 0 .and. esc_hex_val <= 255) then
1162 temp_output(out_pos:out_pos) = char(esc_hex_val)
1163 out_pos = out_pos + 1
1164 end if
1165 cycle ! i already advanced past octal digits
1166 case default
1167 ! Unknown escape - preserve backslash and character
1168 temp_output(out_pos:out_pos+1) = '\' // input(i:i)
1169 out_pos = out_pos + 2
1170 end select
1171 i = i + 1
1172 else
1173 temp_output(out_pos:out_pos) = input(i:i)
1174 out_pos = out_pos + 1
1175 i = i + 1
1176 end if
1177 end do
1178
1179 output = temp_output(1:out_pos-1)
1180 deallocate(temp_output)
1181 end function
1182
1183 ! Pattern replacement in string
1184 subroutine pattern_replace(input, pattern, replacement, replace_all, output)
1185 character(len=*), intent(in) :: input, pattern, replacement
1186 logical, intent(in) :: replace_all
1187 character(len=:), allocatable, intent(out) :: output
1188 #ifdef USE_C_STRINGS
1189 ! flang-new path: route through C to avoid allocatable heap corruption
1190 integer :: in_len, pat_len, repl_len, result_len, rc
1191 integer(c_int) :: c_replace_all
1192 type(c_ptr) :: c_result_ptr, ebuf
1193 integer(c_size_t) :: copied
1194 character(kind=c_char), pointer :: raw(:)
1195
1196 in_len = len_trim(input)
1197 pat_len = len_trim(pattern)
1198 repl_len = len_trim(replacement)
1199
1200 if (pat_len == 0) then
1201 if (in_len > 0) then
1202 ebuf = c_buf_create(int(in_len + 1, c_size_t))
1203 rc = c_buf_append_chars(ebuf, input, int(in_len, c_size_t))
1204 allocate(character(len=in_len) :: output)
1205 copied = c_buf_to_fortran(ebuf, output, int(in_len, c_size_t))
1206 call c_buf_destroy(ebuf)
1207 else
1208 output = ''
1209 end if
1210 return
1211 end if
1212
1213 if (replace_all) then
1214 c_replace_all = 1_c_int
1215 else
1216 c_replace_all = 0_c_int
1217 end if
1218
1219 ebuf = c_buf_create(int(in_len + 1, c_size_t))
1220 rc = c_buf_append_chars(ebuf, input, int(in_len, c_size_t))
1221 result_len = c_pattern_replace_alloc(input, int(in_len, c_int), &
1222 pattern, int(pat_len, c_int), &
1223 replacement, int(repl_len, c_int), &
1224 c_replace_all, c_result_ptr)
1225 call c_buf_destroy(ebuf)
1226
1227 if (result_len > 0) then
1228 call c_f_pointer(c_result_ptr, raw, [result_len])
1229 ebuf = c_buf_create(int(result_len + 1, c_size_t))
1230 rc = c_buf_append_chars(ebuf, raw, int(result_len, c_size_t))
1231 allocate(character(len=result_len) :: output)
1232 copied = c_buf_to_fortran(ebuf, output, int(result_len, c_size_t))
1233 call c_buf_destroy(ebuf)
1234 else
1235 output = ''
1236 end if
1237 call c_free_string(c_result_ptr)
1238 #else
1239 ! gfortran path: character-by-character scan, no C dependencies
1240 integer :: in_len, pat_len, repl_len, i2, j2
1241 integer :: out_pos, out_cap
1242 logical :: matched
1243 character(len=:), allocatable :: result_buf
1244
1245 in_len = len_trim(input)
1246 pat_len = len_trim(pattern)
1247 repl_len = len_trim(replacement)
1248
1249 if (pat_len == 0) then
1250 output = input(1:in_len)
1251 return
1252 end if
1253
1254 if (repl_len > pat_len) then
1255 out_cap = in_len + (in_len / pat_len + 1) * (repl_len - pat_len) + 1
1256 else
1257 out_cap = in_len + 1
1258 end if
1259 allocate(character(len=out_cap) :: result_buf)
1260 out_pos = 1
1261
1262 i2 = 1
1263 do while (i2 <= in_len)
1264 ! Try glob pattern match at position i2 — find shortest match
1265 matched = .false.
1266 do j2 = 1, in_len - i2 + 1
1267 if (pattern_matches_no_dotfile_check(pattern(1:pat_len), input(i2:i2+j2-1))) then
1268 matched = .true.
1269 exit ! j2 = matched length
1270 end if
1271 end do
1272 if (matched) then
1273 if (repl_len > 0) then
1274 result_buf(out_pos:out_pos + repl_len - 1) = replacement(1:repl_len)
1275 out_pos = out_pos + repl_len
1276 end if
1277 i2 = i2 + j2 ! skip matched portion
1278 if (.not. replace_all) then
1279 do while (i2 <= in_len)
1280 result_buf(out_pos:out_pos) = input(i2:i2)
1281 out_pos = out_pos + 1
1282 i2 = i2 + 1
1283 end do
1284 exit
1285 end if
1286 else
1287 result_buf(out_pos:out_pos) = input(i2:i2)
1288 out_pos = out_pos + 1
1289 i2 = i2 + 1
1290 end if
1291 end do
1292 if (out_pos > 1) then
1293 output = result_buf(1:out_pos - 1)
1294 else
1295 output = ''
1296 end if
1297 deallocate(result_buf)
1298 #endif
1299 end subroutine
1300
1301 #ifdef USE_C_STRINGS
1302 ! Pattern replace: C buffer in → C replace → result stored in C buffer,
1303 ! extracted to Fortran allocatable via c_buf_to_fortran (single memcpy).
1304 subroutine pattern_replace_cbuf_to_expanded(input_buf, in_len, pattern, replacement, &
1305 replace_all, output)
1306 type(c_ptr), intent(in) :: input_buf
1307 integer, intent(in) :: in_len
1308 character(len=*), intent(in) :: pattern, replacement
1309 logical, intent(in) :: replace_all
1310 character(len=:), allocatable, intent(out) :: output
1311 integer :: pat_len, repl_len, result_len, rc
1312 integer(c_int) :: c_replace_all
1313 type(c_ptr) :: c_result_ptr, rbuf
1314 integer(c_size_t) :: copied, buf_len
1315 character(kind=c_char), pointer :: raw(:)
1316
1317 pat_len = len_trim(pattern)
1318 repl_len = len_trim(replacement)
1319
1320 if (pat_len == 0 .or. in_len == 0) then
1321 if (in_len > 0) then
1322 allocate(character(len=in_len) :: output)
1323 copied = c_buf_to_fortran(input_buf, output, int(in_len, c_size_t))
1324 else
1325 output = ''
1326 end if
1327 return
1328 end if
1329
1330 if (replace_all) then
1331 c_replace_all = 1_c_int
1332 else
1333 c_replace_all = 0_c_int
1334 end if
1335
1336 result_len = c_buf_pattern_replace(input_buf, pattern, int(pat_len, c_int), &
1337 replacement, int(repl_len, c_int), &
1338 c_replace_all, c_result_ptr)
1339
1340 if (result_len > 0) then
1341 ! Wrap C result in a buffer, then extract to Fortran in one memcpy
1342 call c_f_pointer(c_result_ptr, raw, [result_len])
1343 rbuf = c_buf_create(int(result_len + 1, c_size_t))
1344 rc = c_buf_append_chars(rbuf, raw, int(result_len, c_size_t))
1345 allocate(character(len=result_len) :: output)
1346 copied = c_buf_to_fortran(rbuf, output, int(result_len, c_size_t))
1347 call c_buf_destroy(rbuf)
1348 else
1349 output = ''
1350 end if
1351
1352 call c_free_string(c_result_ptr)
1353 end subroutine
1354 #endif
1355
1356 ! Remove suffix matching pattern (greedy or non-greedy)
1357 subroutine remove_suffix(input, pattern, greedy, output)
1358 character(len=*), intent(in) :: input, pattern
1359 logical, intent(in) :: greedy
1360 character(len=:), allocatable, intent(out) :: output
1361 integer :: best_pos, i
1362
1363 output = input(1:len_trim(input))
1364
1365 if (len_trim(pattern) == 0) return
1366
1367 if (greedy) then
1368 ! Remove largest matching suffix (%%)
1369 ! Try to match from start of string
1370 best_pos = 0
1371 do i = 1, len_trim(input)
1372 if (match_pattern(input(i:), trim(pattern))) then
1373 best_pos = i
1374 exit
1375 end if
1376 end do
1377 if (best_pos > 0) then
1378 output = input(1:best_pos-1)
1379 end if
1380 else
1381 ! Remove smallest matching suffix (%)
1382 ! Try to match from end of string - include len_trim+1 for empty suffix
1383 do i = len_trim(input) + 1, 1, -1
1384 if (match_pattern(input(i:), trim(pattern))) then
1385 output = input(1:i-1)
1386 return
1387 end if
1388 end do
1389 end if
1390 end subroutine
1391
1392 ! Remove prefix matching pattern (greedy or non-greedy)
1393 subroutine remove_prefix(input, pattern, greedy, output)
1394 character(len=*), intent(in) :: input, pattern
1395 logical, intent(in) :: greedy
1396 character(len=:), allocatable, intent(out) :: output
1397 integer :: best_pos, i
1398
1399 output = input(1:len_trim(input))
1400
1401 if (len_trim(pattern) == 0) return
1402
1403 if (greedy) then
1404 ! Remove largest matching prefix (##)
1405 ! Try to match from end, working backwards
1406 best_pos = 0
1407 do i = len_trim(input), 1, -1
1408 if (match_pattern(input(1:i), trim(pattern))) then
1409 best_pos = i
1410 exit
1411 end if
1412 end do
1413 if (best_pos > 0) then
1414 output = input(best_pos+1:)
1415 end if
1416 else
1417 ! Remove smallest matching prefix (#)
1418 ! Try to match from start - start at 0 to test empty prefix
1419 do i = 0, len_trim(input)
1420 if (match_pattern(input(1:i), trim(pattern))) then
1421 output = input(i+1:)
1422 return
1423 end if
1424 end do
1425 end if
1426 end subroutine
1427
1428 ! Simple pattern matching (supports * and ? wildcards)
1429 ! Delegate to glob module's pattern matcher which handles *, ?, [...]
1430 function match_pattern(str, pattern) result(matches)
1431 character(len=*), intent(in) :: str, pattern
1432 logical :: matches
1433 matches = pattern_matches_no_dotfile_check(pattern, str)
1434 end function
1435
1436 ! ============================================================================
1437 ! Arithmetic Expansion: $((expression))
1438 ! Comprehensive arithmetic evaluator with full operator support
1439 ! ============================================================================
1440
1441 ! Note: This version doesn't have shell context - used when called from parser
1442 function arithmetic_expansion(expression) result(result_value)
1443 character(len=*), intent(in) :: expression
1444 character(len=32) :: result_value
1445 character(len=512) :: expr
1446 integer(kind=8) :: result_int
1447
1448 result_value = '0'
1449
1450 ! Remove $(( and ))
1451 if (len_trim(expression) < 6) return
1452 expr = adjustl(expression(4:len_trim(expression)-2))
1453
1454 ! Clear any previous error
1455 arithmetic_error = .false.
1456 arithmetic_error_msg = ''
1457
1458 ! Evaluate the arithmetic expression (without shell context for variable resolution)
1459 result_int = eval_expression(trim(expr))
1460
1461 ! Check for arithmetic errors
1462 if (arithmetic_error) then
1463 write(error_unit, '(a,a)') 'fortsh: arithmetic expression: ', trim(arithmetic_error_msg)
1464 result_value = '' ! Return empty string to signal error
1465 else
1466 write(result_value, '(I0)') result_int
1467 end if
1468 end function
1469
1470 ! Version with shell context for variable resolution
1471 function arithmetic_expansion_shell(expression, shell) result(result_value)
1472 character(len=*), intent(in) :: expression
1473 type(shell_state_t), intent(inout) :: shell
1474 character(len=32) :: result_value
1475 character(len=512) :: expr
1476 character(len=:), allocatable :: expanded_expr
1477 integer(kind=8) :: result_int
1478
1479 result_value = '0'
1480
1481 ! Remove $(( and ))
1482 if (len_trim(expression) < 6) return
1483 expr = adjustl(expression(4:len_trim(expression)-2))
1484
1485 ! Clear any previous error
1486 arithmetic_error = .false.
1487 arithmetic_error_msg = ''
1488 shell%arithmetic_error = .false.
1489 shell%arithmetic_error_msg = ''
1490
1491 ! Expand ALL parameter expansions ($var, $1, $(cmd), etc.) before evaluation
1492 ! This handles variables, positional parameters, and command substitutions
1493 ! NOTE: Only call enhanced_expand_variables if there are $ characters to expand,
1494 ! because it has a bug where it strips internal whitespace.
1495 if (index(expr, '$') > 0) then
1496 call enhanced_expand_variables(expr, expanded_expr, shell)
1497 else
1498 expanded_expr = trim(expr)
1499 end if
1500
1501 ! Evaluate with shell context for any remaining variable resolution
1502 result_int = eval_expression_shell(trim(expanded_expr), shell)
1503
1504 ! Check for arithmetic errors
1505 if (arithmetic_error) then
1506 write(error_unit, '(a,a)') 'fortsh: arithmetic expression: ', trim(arithmetic_error_msg)
1507 shell%last_exit_status = 1 ! bash returns 1 for arithmetic errors
1508 shell%arithmetic_error = .true.
1509 shell%arithmetic_error_msg = trim(arithmetic_error_msg)
1510 result_value = '' ! Return empty string to signal error
1511 else
1512 write(result_value, '(I0)') result_int
1513 end if
1514 end function
1515
1516 ! Main expression evaluator - handles full expressions
1517 recursive function eval_expression(expr) result(value)
1518 character(len=*), intent(in) :: expr
1519 integer(kind=8) :: value
1520
1521 value = eval_ternary(trim(adjustl(expr)))
1522 end function
1523
1524 ! Ternary conditional operator (? :)
1525 recursive function eval_ternary(expr) result(value)
1526 character(len=*), intent(in) :: expr
1527 integer(kind=8) :: value
1528 integer :: qmark_pos, colon_pos, depth, i
1529 character(len=512) :: condition_expr, true_expr, false_expr
1530
1531 ! Find ? outside parentheses
1532 qmark_pos = 0
1533 depth = 0
1534 do i = 1, len_trim(expr)
1535 if (expr(i:i) == '(') then
1536 depth = depth + 1
1537 else if (expr(i:i) == ')') then
1538 depth = depth - 1
1539 else if (depth == 0 .and. expr(i:i) == '?') then
1540 qmark_pos = i
1541 exit
1542 end if
1543 end do
1544
1545 if (qmark_pos > 0) then
1546 ! Find matching : after the ?
1547 colon_pos = 0
1548 depth = 0
1549 do i = qmark_pos + 1, len_trim(expr)
1550 if (expr(i:i) == '(') then
1551 depth = depth + 1
1552 else if (expr(i:i) == ')') then
1553 depth = depth - 1
1554 else if (depth == 0 .and. expr(i:i) == ':') then
1555 colon_pos = i
1556 exit
1557 end if
1558 end do
1559
1560 if (colon_pos > 0) then
1561 condition_expr = expr(:qmark_pos-1)
1562 true_expr = expr(qmark_pos+1:colon_pos-1)
1563 false_expr = expr(colon_pos+1:)
1564
1565 ! Evaluate condition
1566 value = eval_logical_or(trim(adjustl(condition_expr)))
1567 if (value /= 0) then
1568 ! Condition is true, evaluate true expression
1569 value = eval_ternary(trim(adjustl(true_expr)))
1570 else
1571 ! Condition is false, evaluate false expression
1572 value = eval_ternary(trim(adjustl(false_expr)))
1573 end if
1574 return
1575 end if
1576 end if
1577
1578 ! No ternary operator found
1579 value = eval_logical_or(expr)
1580 end function
1581
1582 ! Logical OR (lowest precedence except ternary)
1583 recursive function eval_logical_or(expr) result(value)
1584 character(len=*), intent(in) :: expr
1585 integer(kind=8) :: value, right_val
1586 integer :: pos
1587 character(len=512) :: left_expr, right_expr
1588
1589 value = eval_logical_and(expr)
1590
1591 pos = find_operator(expr, '||')
1592 if (pos > 0) then
1593 left_expr = expr(:pos-1)
1594 right_expr = expr(pos+2:)
1595 value = eval_logical_and(trim(adjustl(left_expr)))
1596 right_val = eval_logical_or(trim(adjustl(right_expr)))
1597 if (value /= 0 .or. right_val /= 0) then
1598 value = 1
1599 else
1600 value = 0
1601 end if
1602 end if
1603 end function
1604
1605 ! Logical AND
1606 recursive function eval_logical_and(expr) result(value)
1607 character(len=*), intent(in) :: expr
1608 integer(kind=8) :: value, right_val
1609 integer :: pos
1610 character(len=512) :: left_expr, right_expr
1611
1612 value = eval_bitwise_or(expr)
1613
1614 pos = find_operator(expr, '&&')
1615 if (pos > 0) then
1616 left_expr = expr(:pos-1)
1617 right_expr = expr(pos+2:)
1618 value = eval_bitwise_or(trim(adjustl(left_expr)))
1619 right_val = eval_logical_and(trim(adjustl(right_expr)))
1620 if (value /= 0 .and. right_val /= 0) then
1621 value = 1
1622 else
1623 value = 0
1624 end if
1625 end if
1626 end function
1627
1628 ! Bitwise OR
1629 recursive function eval_bitwise_or(expr) result(value)
1630 character(len=*), intent(in) :: expr
1631 integer(kind=8) :: value, right_val
1632 integer :: pos
1633 character(len=512) :: left_expr, right_expr
1634
1635 value = eval_bitwise_xor(expr)
1636
1637 pos = find_single_operator(expr, '|')
1638 if (pos > 0) then
1639 left_expr = expr(:pos-1)
1640 right_expr = expr(pos+1:)
1641 value = eval_bitwise_xor(trim(adjustl(left_expr)))
1642 right_val = eval_bitwise_or(trim(adjustl(right_expr)))
1643 value = ior(int(value), int(right_val))
1644 end if
1645 end function
1646
1647 ! Bitwise XOR
1648 recursive function eval_bitwise_xor(expr) result(value)
1649 character(len=*), intent(in) :: expr
1650 integer(kind=8) :: value, right_val
1651 integer :: pos
1652 character(len=512) :: left_expr, right_expr
1653
1654 value = eval_bitwise_and(expr)
1655
1656 pos = find_single_operator(expr, '^')
1657 if (pos > 0) then
1658 left_expr = expr(:pos-1)
1659 right_expr = expr(pos+1:)
1660 value = eval_bitwise_and(trim(adjustl(left_expr)))
1661 right_val = eval_bitwise_xor(trim(adjustl(right_expr)))
1662 value = ieor(int(value), int(right_val))
1663 end if
1664 end function
1665
1666 ! Bitwise AND
1667 recursive function eval_bitwise_and(expr) result(value)
1668 character(len=*), intent(in) :: expr
1669 integer(kind=8) :: value, right_val
1670 integer :: pos
1671 character(len=512) :: left_expr, right_expr
1672
1673 value = eval_equality(expr)
1674
1675 pos = find_single_operator(expr, '&')
1676 if (pos > 0) then
1677 left_expr = expr(:pos-1)
1678 right_expr = expr(pos+1:)
1679 value = eval_equality(trim(adjustl(left_expr)))
1680 right_val = eval_bitwise_and(trim(adjustl(right_expr)))
1681 value = iand(int(value), int(right_val))
1682 end if
1683 end function
1684
1685 ! Equality (==, !=)
1686 recursive function eval_equality(expr) result(value)
1687 character(len=*), intent(in) :: expr
1688 integer(kind=8) :: value, right_val
1689 integer :: pos
1690 character(len=512) :: left_expr, right_expr
1691
1692 ! Try ==
1693 pos = find_operator(expr, '==')
1694 if (pos > 0) then
1695 left_expr = expr(:pos-1)
1696 right_expr = expr(pos+2:)
1697 value = eval_comparison(trim(adjustl(left_expr)))
1698 right_val = eval_comparison(trim(adjustl(right_expr)))
1699 if (value == right_val) then
1700 value = 1
1701 else
1702 value = 0
1703 end if
1704 return
1705 end if
1706
1707 ! Try !=
1708 pos = find_operator(expr, '!=')
1709 if (pos > 0) then
1710 left_expr = expr(:pos-1)
1711 right_expr = expr(pos+2:)
1712 value = eval_comparison(trim(adjustl(left_expr)))
1713 right_val = eval_comparison(trim(adjustl(right_expr)))
1714 if (value /= right_val) then
1715 value = 1
1716 else
1717 value = 0
1718 end if
1719 return
1720 end if
1721
1722 value = eval_comparison(expr)
1723 end function
1724
1725 ! Comparison (<, <=, >, >=)
1726 recursive function eval_comparison(expr) result(value)
1727 character(len=*), intent(in) :: expr
1728 integer(kind=8) :: value, right_val
1729 integer :: pos
1730 character(len=512) :: left_expr, right_expr
1731
1732 ! Try <=
1733 pos = find_operator(expr, '<=')
1734 if (pos > 0) then
1735 left_expr = expr(:pos-1)
1736 right_expr = expr(pos+2:)
1737 value = eval_shift(trim(adjustl(left_expr)))
1738 right_val = eval_shift(trim(adjustl(right_expr)))
1739 if (value <= right_val) then
1740 value = 1
1741 else
1742 value = 0
1743 end if
1744 return
1745 end if
1746
1747 ! Try >=
1748 pos = find_operator(expr, '>=')
1749 if (pos > 0) then
1750 left_expr = expr(:pos-1)
1751 right_expr = expr(pos+2:)
1752 value = eval_shift(trim(adjustl(left_expr)))
1753 right_val = eval_shift(trim(adjustl(right_expr)))
1754 if (value >= right_val) then
1755 value = 1
1756 else
1757 value = 0
1758 end if
1759 return
1760 end if
1761
1762 ! Try <
1763 pos = find_single_operator(expr, '<')
1764 if (pos > 0) then
1765 left_expr = expr(:pos-1)
1766 right_expr = expr(pos+1:)
1767 value = eval_shift(trim(adjustl(left_expr)))
1768 right_val = eval_shift(trim(adjustl(right_expr)))
1769 if (value < right_val) then
1770 value = 1
1771 else
1772 value = 0
1773 end if
1774 return
1775 end if
1776
1777 ! Try >
1778 pos = find_single_operator(expr, '>')
1779 if (pos > 0) then
1780 left_expr = expr(:pos-1)
1781 right_expr = expr(pos+1:)
1782 value = eval_shift(trim(adjustl(left_expr)))
1783 right_val = eval_shift(trim(adjustl(right_expr)))
1784 if (value > right_val) then
1785 value = 1
1786 else
1787 value = 0
1788 end if
1789 return
1790 end if
1791
1792 value = eval_shift(expr)
1793 end function
1794
1795 ! Shift operations (<<, >>)
1796 recursive function eval_shift(expr) result(value)
1797 character(len=*), intent(in) :: expr
1798 integer(kind=8) :: value, right_val
1799 integer :: pos
1800 character(len=512) :: left_expr, right_expr
1801
1802 ! Try << (left shift)
1803 pos = find_operator(expr, '<<')
1804 if (pos > 0) then
1805 left_expr = expr(:pos-1)
1806 right_expr = expr(pos+2:)
1807 value = eval_additive(trim(adjustl(left_expr))) ! Changed from eval_shift
1808 right_val = eval_additive(trim(adjustl(right_expr)))
1809 ! Left shift by right_val bits
1810 value = ishft(value, int(right_val))
1811 return
1812 end if
1813
1814 ! Try >> (right shift)
1815 pos = find_operator(expr, '>>')
1816 if (pos > 0) then
1817 left_expr = expr(:pos-1)
1818 right_expr = expr(pos+2:)
1819 value = eval_additive(trim(adjustl(left_expr))) ! Changed from eval_shift
1820 right_val = eval_additive(trim(adjustl(right_expr)))
1821 ! Right shift by right_val bits (negative for right shift in ishft)
1822 value = ishft(value, -int(right_val))
1823 return
1824 end if
1825
1826 ! No shift operator found
1827 value = eval_additive(expr)
1828 end function
1829
1830 ! Addition and Subtraction
1831 recursive function eval_additive(expr) result(value)
1832 character(len=*), intent(in) :: expr
1833 integer(kind=8) :: value, right_val
1834 integer :: pos
1835 character(len=512) :: left_expr, right_expr
1836
1837 ! Find rightmost + or - (to maintain left-to-right evaluation)
1838 pos = find_rightmost_additive(expr)
1839
1840 if (pos > 0) then
1841 left_expr = expr(:pos-1)
1842 right_expr = expr(pos+1:)
1843 value = eval_additive(trim(adjustl(left_expr)))
1844 right_val = eval_multiplicative(trim(adjustl(right_expr)))
1845
1846 if (expr(pos:pos) == '+') then
1847 value = value + right_val
1848 else
1849 value = value - right_val
1850 end if
1851 else
1852 value = eval_multiplicative(expr)
1853 end if
1854 end function
1855
1856 ! Multiplication, Division, Modulo
1857 recursive function eval_multiplicative(expr) result(value)
1858 character(len=*), intent(in) :: expr
1859 integer(kind=8) :: value, right_val
1860 integer :: pos
1861 character(len=512) :: left_expr, right_expr
1862 character :: op
1863
1864 ! Find rightmost *, /, or %
1865 pos = find_rightmost_multiplicative(expr, op)
1866
1867 if (pos > 0) then
1868 left_expr = expr(:pos-1)
1869 right_expr = expr(pos+1:)
1870 value = eval_multiplicative(trim(adjustl(left_expr)))
1871 right_val = eval_power(trim(adjustl(right_expr)))
1872
1873 select case (op)
1874 case ('*')
1875 value = value * right_val
1876 case ('/')
1877 if (right_val /= 0) then
1878 value = value / right_val
1879 else
1880 arithmetic_error = .true.
1881 arithmetic_error_msg = 'division by zero'
1882 value = 0 ! Division by zero
1883 end if
1884 case ('%')
1885 if (right_val /= 0) then
1886 value = mod(value, right_val)
1887 else
1888 arithmetic_error = .true.
1889 arithmetic_error_msg = 'division by zero'
1890 value = 0 ! Modulo by zero
1891 end if
1892 end select
1893 else
1894 value = eval_power(expr)
1895 end if
1896 end function
1897
1898 ! Exponentiation (**)
1899 recursive function eval_power(expr) result(value)
1900 character(len=*), intent(in) :: expr
1901 integer(kind=8) :: value, exponent
1902 integer :: pos, i
1903 character(len=512) :: base_expr, exp_expr
1904
1905 pos = find_operator(expr, '**')
1906 if (pos > 0) then
1907 base_expr = expr(:pos-1)
1908 exp_expr = expr(pos+2:)
1909 value = eval_unary(trim(adjustl(base_expr)))
1910 exponent = eval_power(trim(adjustl(exp_expr))) ! Right-associative
1911
1912 ! Calculate power
1913 if (exponent < 0) then
1914 value = 0 ! Integer division for negative exponents
1915 else if (exponent == 0) then
1916 value = 1
1917 else
1918 do i = 2, int(exponent)
1919 value = value * eval_unary(trim(adjustl(base_expr)))
1920 end do
1921 end if
1922 else
1923 value = eval_unary(expr)
1924 end if
1925 end function
1926
1927 ! Unary operators (!, -, +)
1928 recursive function eval_unary(expr) result(value)
1929 character(len=*), intent(in) :: expr
1930 integer(kind=8) :: value
1931 character(len=512) :: rest
1932
1933 if (len_trim(expr) == 0) then
1934 value = 0
1935 return
1936 end if
1937
1938 ! Logical NOT
1939 if (expr(1:1) == '!') then
1940 rest = adjustl(expr(2:))
1941 value = eval_unary(rest)
1942 if (value == 0) then
1943 value = 1
1944 else
1945 value = 0
1946 end if
1947 return
1948 end if
1949
1950 ! Bitwise NOT (~)
1951 if (expr(1:1) == '~') then
1952 rest = adjustl(expr(2:))
1953 value = eval_unary(rest)
1954 ! Bitwise NOT in two's complement: ~n = -(n + 1)
1955 value = -(value + 1)
1956 return
1957 end if
1958
1959 ! Unary minus
1960 if (expr(1:1) == '-' .and. len_trim(expr) > 1) then
1961 rest = adjustl(expr(2:))
1962 value = -eval_unary(rest)
1963 return
1964 end if
1965
1966 ! Unary plus
1967 if (expr(1:1) == '+' .and. len_trim(expr) > 1) then
1968 rest = adjustl(expr(2:))
1969 value = eval_unary(rest)
1970 return
1971 end if
1972
1973 value = eval_primary(expr)
1974 end function
1975
1976 ! Primary expressions (numbers, variables, parentheses)
1977 function eval_primary(expr) result(value)
1978 character(len=*), intent(in) :: expr
1979 integer(kind=8) :: value
1980 character(len=512) :: inner_expr, temp_expr
1981 integer :: iostat, paren_end
1982
1983 if (len_trim(expr) == 0) then
1984 value = 0
1985 return
1986 end if
1987
1988 ! Handle parentheses
1989 if (expr(1:1) == '(') then
1990 paren_end = find_matching_paren(expr, 1)
1991 if (paren_end > 1) then
1992 inner_expr = expr(2:paren_end-1)
1993 value = eval_expression(trim(adjustl(inner_expr)))
1994 return
1995 end if
1996 end if
1997
1998 ! Try to parse as number (with octal/hex support)
1999 temp_expr = trim(adjustl(expr))
2000 value = parse_arithmetic_number(temp_expr, iostat)
2001 if (iostat == 0) return
2002
2003 ! Variable without shell context - return 0
2004 value = 0
2005 end function
2006
2007 ! ============================================================================
2008 ! Shell-aware arithmetic evaluation (with variable resolution)
2009 ! ============================================================================
2010
2011 recursive function eval_expression_shell(expr, shell) result(value)
2012 character(len=*), intent(in) :: expr
2013 type(shell_state_t), intent(inout) :: shell
2014 integer(kind=8) :: value
2015 ! Comma operator has lowest precedence
2016 value = eval_comma_shell(trim(adjustl(expr)), shell)
2017 end function
2018
2019 ! Comma operator (evaluates left-to-right, returns rightmost value)
2020 recursive function eval_comma_shell(expr, shell) result(value)
2021 character(len=*), intent(in) :: expr
2022 type(shell_state_t), intent(inout) :: shell
2023 integer(kind=8) :: value
2024 integer :: comma_pos, paren_depth, i
2025 character(len=1) :: ch
2026
2027 ! Find comma at top level (not inside parentheses)
2028 paren_depth = 0
2029 comma_pos = 0
2030 do i = 1, len_trim(expr)
2031 ch = expr(i:i)
2032 if (ch == '(') then
2033 paren_depth = paren_depth + 1
2034 else if (ch == ')') then
2035 paren_depth = paren_depth - 1
2036 else if (ch == ',' .and. paren_depth == 0) then
2037 ! Evaluate left side (for side effects), then continue with right
2038 value = eval_assignment_shell(trim(adjustl(expr(:i-1))), shell)
2039 ! Continue evaluating right side (may have more commas)
2040 value = eval_comma_shell(trim(adjustl(expr(i+1:))), shell)
2041 return
2042 end if
2043 end do
2044
2045 ! No comma found, evaluate as assignment
2046 value = eval_assignment_shell(trim(adjustl(expr)), shell)
2047 end function
2048
2049 ! Assignment operators (=, +=, -=, *=, /=, %=)
2050 recursive function eval_assignment_shell(expr, shell) result(value)
2051 character(len=*), intent(in) :: expr
2052 type(shell_state_t), intent(inout) :: shell
2053 integer(kind=8) :: value, right_val, current_val
2054 integer :: pos, op_len, iostat
2055 character(len=512) :: var_name, right_expr, var_value_str
2056 character(len=:), allocatable :: temp_value
2057
2058 ! Check for assignment operators (right-to-left associative, so find rightmost)
2059 pos = find_rightmost_assignment(expr, op_len)
2060
2061 if (pos > 0) then
2062 ! Extract variable name (left side) and expression (right side)
2063 var_name = trim(adjustl(expr(:pos-1)))
2064 right_expr = expr(pos+op_len:)
2065
2066 ! Evaluate right side
2067 right_val = eval_assignment_shell(trim(adjustl(right_expr)), shell)
2068
2069 ! Determine which operator and perform operation
2070 if (op_len == 1) then
2071 ! Simple assignment: =
2072 value = right_val
2073 else
2074 ! Compound assignment - get current value
2075 temp_value = get_shell_variable(shell, trim(var_name))
2076 if (len_trim(temp_value) > 0) then
2077 read(temp_value, *, iostat=iostat) current_val
2078 if (iostat /= 0) current_val = 0
2079 else
2080 current_val = 0
2081 end if
2082
2083 ! Apply compound operator
2084 select case (expr(pos:pos+op_len-1))
2085 case ('+=')
2086 value = current_val + right_val
2087 case ('-=')
2088 value = current_val - right_val
2089 case ('*=')
2090 value = current_val * right_val
2091 case ('/=')
2092 if (right_val /= 0) then
2093 value = current_val / right_val
2094 else
2095 arithmetic_error = .true.
2096 arithmetic_error_msg = 'division by zero'
2097 value = 0
2098 end if
2099 case ('%=')
2100 if (right_val /= 0) then
2101 value = mod(current_val, right_val)
2102 else
2103 arithmetic_error = .true.
2104 arithmetic_error_msg = 'division by zero'
2105 value = 0
2106 end if
2107 case default
2108 value = right_val
2109 end select
2110 end if
2111
2112 ! Set the variable
2113 write(var_value_str, '(I0)') value
2114 call set_shell_variable(shell, trim(var_name), trim(var_value_str))
2115 else
2116 ! No assignment, evaluate as ternary
2117 value = eval_ternary_shell(expr, shell)
2118 end if
2119 end function
2120
2121 ! Ternary conditional operator (? :)
2122 recursive function eval_ternary_shell(expr, shell) result(value)
2123 character(len=*), intent(in) :: expr
2124 type(shell_state_t), intent(inout) :: shell
2125 integer(kind=8) :: value
2126 integer :: qmark_pos, colon_pos, depth, i
2127 character(len=512) :: condition_expr, true_expr, false_expr
2128
2129 ! Find ? outside parentheses
2130 qmark_pos = 0
2131 depth = 0
2132 do i = 1, len_trim(expr)
2133 if (expr(i:i) == '(') then
2134 depth = depth + 1
2135 else if (expr(i:i) == ')') then
2136 depth = depth - 1
2137 else if (depth == 0 .and. expr(i:i) == '?') then
2138 qmark_pos = i
2139 exit
2140 end if
2141 end do
2142
2143 if (qmark_pos > 0) then
2144 ! Find matching : after the ?
2145 colon_pos = 0
2146 depth = 0
2147 do i = qmark_pos + 1, len_trim(expr)
2148 if (expr(i:i) == '(') then
2149 depth = depth + 1
2150 else if (expr(i:i) == ')') then
2151 depth = depth - 1
2152 else if (depth == 0 .and. expr(i:i) == ':') then
2153 colon_pos = i
2154 exit
2155 end if
2156 end do
2157
2158 if (colon_pos > 0) then
2159 condition_expr = expr(:qmark_pos-1)
2160 true_expr = expr(qmark_pos+1:colon_pos-1)
2161 false_expr = expr(colon_pos+1:)
2162
2163 ! Evaluate condition
2164 value = eval_logical_or_shell(trim(adjustl(condition_expr)), shell)
2165 if (value /= 0) then
2166 ! Condition is true, evaluate true expression
2167 value = eval_ternary_shell(trim(adjustl(true_expr)), shell)
2168 else
2169 ! Condition is false, evaluate false expression
2170 value = eval_ternary_shell(trim(adjustl(false_expr)), shell)
2171 end if
2172 return
2173 end if
2174 end if
2175
2176 ! No ternary operator found
2177 value = eval_logical_or_shell(expr, shell)
2178 end function
2179
2180 ! Helper function to find leftmost assignment operator (for right-associativity)
2181 function find_rightmost_assignment(expr, op_len) result(pos)
2182 character(len=*), intent(in) :: expr
2183 integer, intent(out) :: op_len
2184 integer :: pos, i, paren_depth
2185
2186 pos = 0
2187 op_len = 0
2188 paren_depth = 0
2189
2190 ! Scan from left to right, tracking parentheses
2191 ! This gives right-associativity: a=b=c becomes a=(b=c)
2192 do i = 1, len_trim(expr)
2193 if (expr(i:i) == '(') then
2194 paren_depth = paren_depth + 1
2195 else if (expr(i:i) == ')') then
2196 paren_depth = paren_depth - 1
2197 else if (paren_depth == 0) then
2198 ! Check for compound assignment operators (2 chars)
2199 if (i < len_trim(expr)) then
2200 if (expr(i:i+1) == '+=' .or. expr(i:i+1) == '-=' .or. &
2201 expr(i:i+1) == '*=' .or. expr(i:i+1) == '/=' .or. &
2202 expr(i:i+1) == '%=') then
2203 pos = i
2204 op_len = 2
2205 return
2206 end if
2207 end if
2208 ! Check for simple assignment (but not ==, !=, <=, >=)
2209 if (expr(i:i) == '=') then
2210 ! Check it's not a comparison operator
2211 if (i > 1) then
2212 if (expr(i-1:i-1) == '=' .or. expr(i-1:i-1) == '!' .or. &
2213 expr(i-1:i-1) == '<' .or. expr(i-1:i-1) == '>') then
2214 cycle ! Skip this =, it's part of a comparison
2215 end if
2216 end if
2217 if (i < len_trim(expr)) then
2218 if (expr(i+1:i+1) == '=') then
2219 cycle ! Skip this =, it's part of ==
2220 end if
2221 end if
2222 pos = i
2223 op_len = 1
2224 return
2225 end if
2226 end if
2227 end do
2228 end function
2229
2230 recursive function eval_logical_or_shell(expr, shell) result(value)
2231 character(len=*), intent(in) :: expr
2232 type(shell_state_t), intent(inout) :: shell
2233 integer(kind=8) :: value, right_val
2234 integer :: pos
2235 character(len=512) :: left_expr, right_expr
2236
2237 ! FIRST check for || operator (lowest precedence in logical chain)
2238 pos = find_operator(expr, '||')
2239 if (pos > 0) then
2240 left_expr = expr(:pos-1)
2241 right_expr = expr(pos+2:)
2242 value = eval_logical_and_shell(trim(adjustl(left_expr)), shell)
2243 right_val = eval_logical_or_shell(trim(adjustl(right_expr)), shell)
2244 if (value /= 0 .or. right_val /= 0) then
2245 value = 1
2246 else
2247 value = 0
2248 end if
2249 else
2250 ! No || found, delegate to next precedence level
2251 value = eval_logical_and_shell(expr, shell)
2252 end if
2253 end function
2254
2255 recursive function eval_logical_and_shell(expr, shell) result(value)
2256 character(len=*), intent(in) :: expr
2257 type(shell_state_t), intent(inout) :: shell
2258 integer(kind=8) :: value, right_val
2259 integer :: pos
2260 character(len=512) :: left_expr, right_expr
2261
2262 ! FIRST check for && operator (lowest precedence in this chain)
2263 pos = find_operator(expr, '&&')
2264 if (pos > 0) then
2265 left_expr = expr(:pos-1)
2266 right_expr = expr(pos+2:)
2267 value = eval_bitwise_or_shell(trim(adjustl(left_expr)), shell)
2268 right_val = eval_logical_and_shell(trim(adjustl(right_expr)), shell)
2269 if (value /= 0 .and. right_val /= 0) then
2270 value = 1
2271 else
2272 value = 0
2273 end if
2274 else
2275 ! No && found, delegate to next precedence level
2276 value = eval_bitwise_or_shell(expr, shell)
2277 end if
2278 end function
2279
2280 recursive function eval_bitwise_or_shell(expr, shell) result(value)
2281 character(len=*), intent(in) :: expr
2282 type(shell_state_t), intent(inout) :: shell
2283 integer(kind=8) :: value, right_val
2284 integer :: pos
2285 character(len=512) :: left_expr, right_expr
2286
2287 ! FIRST check for | operator
2288 pos = find_single_operator(expr, '|')
2289 if (pos > 0) then
2290 left_expr = expr(:pos-1)
2291 right_expr = expr(pos+1:)
2292 value = eval_bitwise_xor_shell(trim(adjustl(left_expr)), shell)
2293 right_val = eval_bitwise_or_shell(trim(adjustl(right_expr)), shell)
2294 value = ior(int(value), int(right_val))
2295 else
2296 value = eval_bitwise_xor_shell(expr, shell)
2297 end if
2298 end function
2299
2300 recursive function eval_bitwise_xor_shell(expr, shell) result(value)
2301 character(len=*), intent(in) :: expr
2302 type(shell_state_t), intent(inout) :: shell
2303 integer(kind=8) :: value, right_val
2304 integer :: pos
2305 character(len=512) :: left_expr, right_expr
2306
2307 ! FIRST check for ^ operator
2308 pos = find_single_operator(expr, '^')
2309 if (pos > 0) then
2310 left_expr = expr(:pos-1)
2311 right_expr = expr(pos+1:)
2312 value = eval_bitwise_and_shell(trim(adjustl(left_expr)), shell)
2313 right_val = eval_bitwise_xor_shell(trim(adjustl(right_expr)), shell)
2314 value = ieor(int(value), int(right_val))
2315 else
2316 value = eval_bitwise_and_shell(expr, shell)
2317 end if
2318 end function
2319
2320 recursive function eval_bitwise_and_shell(expr, shell) result(value)
2321 character(len=*), intent(in) :: expr
2322 type(shell_state_t), intent(inout) :: shell
2323 integer(kind=8) :: value, right_val
2324 integer :: pos
2325 character(len=512) :: left_expr, right_expr
2326
2327 ! FIRST check for & operator
2328 pos = find_single_operator(expr, '&')
2329 if (pos > 0) then
2330 left_expr = expr(:pos-1)
2331 right_expr = expr(pos+1:)
2332 value = eval_equality_shell(trim(adjustl(left_expr)), shell)
2333 right_val = eval_bitwise_and_shell(trim(adjustl(right_expr)), shell)
2334 value = iand(int(value), int(right_val))
2335 else
2336 value = eval_equality_shell(expr, shell)
2337 end if
2338 end function
2339
2340 recursive function eval_equality_shell(expr, shell) result(value)
2341 character(len=*), intent(in) :: expr
2342 type(shell_state_t), intent(inout) :: shell
2343 integer(kind=8) :: value, right_val
2344 integer :: pos
2345 character(len=512) :: left_expr, right_expr
2346
2347 pos = find_operator(expr, '==')
2348 if (pos > 0) then
2349 left_expr = expr(:pos-1)
2350 right_expr = expr(pos+2:)
2351 value = eval_comparison_shell(trim(adjustl(left_expr)), shell)
2352 right_val = eval_comparison_shell(trim(adjustl(right_expr)), shell)
2353 if (value == right_val) then
2354 value = 1
2355 else
2356 value = 0
2357 end if
2358 return
2359 end if
2360
2361 pos = find_operator(expr, '!=')
2362 if (pos > 0) then
2363 left_expr = expr(:pos-1)
2364 right_expr = expr(pos+2:)
2365 value = eval_comparison_shell(trim(adjustl(left_expr)), shell)
2366 right_val = eval_comparison_shell(trim(adjustl(right_expr)), shell)
2367 if (value /= right_val) then
2368 value = 1
2369 else
2370 value = 0
2371 end if
2372 return
2373 end if
2374
2375 value = eval_comparison_shell(expr, shell)
2376 end function
2377
2378 recursive function eval_comparison_shell(expr, shell) result(value)
2379 character(len=*), intent(in) :: expr
2380 type(shell_state_t), intent(inout) :: shell
2381 integer(kind=8) :: value, right_val
2382 integer :: pos
2383 character(len=512) :: left_expr, right_expr
2384
2385 pos = find_operator(expr, '<=')
2386 if (pos > 0) then
2387 left_expr = expr(:pos-1)
2388 right_expr = expr(pos+2:)
2389 value = eval_shift_shell(trim(adjustl(left_expr)), shell)
2390 right_val = eval_shift_shell(trim(adjustl(right_expr)), shell)
2391 if (value <= right_val) then; value = 1; else; value = 0; end if
2392 return
2393 end if
2394
2395 pos = find_operator(expr, '>=')
2396 if (pos > 0) then
2397 left_expr = expr(:pos-1)
2398 right_expr = expr(pos+2:)
2399 value = eval_shift_shell(trim(adjustl(left_expr)), shell)
2400 right_val = eval_shift_shell(trim(adjustl(right_expr)), shell)
2401 if (value >= right_val) then; value = 1; else; value = 0; end if
2402 return
2403 end if
2404
2405 pos = find_single_operator(expr, '<')
2406 if (pos > 0) then
2407 left_expr = expr(:pos-1)
2408 right_expr = expr(pos+1:)
2409 value = eval_shift_shell(trim(adjustl(left_expr)), shell)
2410 right_val = eval_shift_shell(trim(adjustl(right_expr)), shell)
2411 if (value < right_val) then; value = 1; else; value = 0; end if
2412 return
2413 end if
2414
2415 pos = find_single_operator(expr, '>')
2416 if (pos > 0) then
2417 left_expr = expr(:pos-1)
2418 right_expr = expr(pos+1:)
2419 value = eval_shift_shell(trim(adjustl(left_expr)), shell)
2420 right_val = eval_shift_shell(trim(adjustl(right_expr)), shell)
2421 if (value > right_val) then; value = 1; else; value = 0; end if
2422 return
2423 end if
2424
2425 value = eval_shift_shell(expr, shell)
2426 end function
2427
2428 ! Shift operations (<<, >>)
2429 recursive function eval_shift_shell(expr, shell) result(value)
2430 character(len=*), intent(in) :: expr
2431 type(shell_state_t), intent(inout) :: shell
2432 integer(kind=8) :: value, right_val
2433 integer :: pos
2434 character(len=512) :: left_expr, right_expr
2435
2436 ! Try << (left shift)
2437 pos = find_operator(expr, '<<')
2438 if (pos > 0) then
2439 left_expr = expr(:pos-1)
2440 right_expr = expr(pos+2:)
2441 value = eval_additive_shell(trim(adjustl(left_expr)), shell) ! Changed from eval_shift_shell
2442 right_val = eval_additive_shell(trim(adjustl(right_expr)), shell)
2443 ! Left shift by right_val bits
2444 value = ishft(value, int(right_val))
2445 return
2446 end if
2447
2448 ! Try >> (right shift)
2449 pos = find_operator(expr, '>>')
2450 if (pos > 0) then
2451 left_expr = expr(:pos-1)
2452 right_expr = expr(pos+2:)
2453 value = eval_additive_shell(trim(adjustl(left_expr)), shell) ! Changed from eval_shift_shell
2454 right_val = eval_additive_shell(trim(adjustl(right_expr)), shell)
2455 ! Right shift by right_val bits (negative for right shift in ishft)
2456 value = ishft(value, -int(right_val))
2457 return
2458 end if
2459
2460 ! No shift operator found
2461 value = eval_additive_shell(expr, shell)
2462 end function
2463
2464 recursive function eval_additive_shell(expr, shell) result(value)
2465 character(len=*), intent(in) :: expr
2466 type(shell_state_t), intent(inout) :: shell
2467 integer(kind=8) :: value, right_val
2468 integer :: pos
2469 character(len=512) :: left_expr, right_expr
2470
2471 pos = find_rightmost_additive(expr)
2472 if (pos > 0) then
2473 left_expr = expr(:pos-1)
2474 right_expr = expr(pos+1:)
2475 value = eval_additive_shell(trim(adjustl(left_expr)), shell)
2476 right_val = eval_multiplicative_shell(trim(adjustl(right_expr)), shell)
2477 if (expr(pos:pos) == '+') then
2478 value = value + right_val
2479 else
2480 value = value - right_val
2481 end if
2482 else
2483 value = eval_multiplicative_shell(expr, shell)
2484 end if
2485 end function
2486
2487 recursive function eval_multiplicative_shell(expr, shell) result(value)
2488 character(len=*), intent(in) :: expr
2489 type(shell_state_t), intent(inout) :: shell
2490 integer(kind=8) :: value, right_val
2491 integer :: pos
2492 character(len=512) :: left_expr, right_expr
2493 character :: op
2494
2495 pos = find_rightmost_multiplicative(expr, op)
2496 if (pos > 0) then
2497 left_expr = expr(:pos-1)
2498 right_expr = expr(pos+1:)
2499 value = eval_multiplicative_shell(trim(adjustl(left_expr)), shell)
2500 right_val = eval_power_shell(trim(adjustl(right_expr)), shell)
2501 select case (op)
2502 case ('*'); value = value * right_val
2503 case ('/')
2504 if (right_val /= 0) then
2505 value = value / right_val
2506 else
2507 arithmetic_error = .true.
2508 arithmetic_error_msg = 'division by zero'
2509 value = 0
2510 end if
2511 case ('%')
2512 if (right_val /= 0) then
2513 value = mod(value, right_val)
2514 else
2515 arithmetic_error = .true.
2516 arithmetic_error_msg = 'division by zero'
2517 value = 0
2518 end if
2519 end select
2520 else
2521 value = eval_power_shell(expr, shell)
2522 end if
2523 end function
2524
2525 recursive function eval_power_shell(expr, shell) result(value)
2526 character(len=*), intent(in) :: expr
2527 type(shell_state_t), intent(inout) :: shell
2528 integer(kind=8) :: value, exponent, base_val
2529 integer :: pos, i
2530 character(len=512) :: base_expr, exp_expr
2531
2532 pos = find_operator(expr, '**')
2533 if (pos > 0) then
2534 base_expr = expr(:pos-1)
2535 exp_expr = expr(pos+2:)
2536 base_val = eval_unary_shell(trim(adjustl(base_expr)), shell)
2537 exponent = eval_power_shell(trim(adjustl(exp_expr)), shell)
2538 if (exponent < 0) then; value = 0
2539 else if (exponent == 0) then; value = 1
2540 else
2541 value = base_val
2542 do i = 2, int(exponent)
2543 value = value * base_val
2544 end do
2545 end if
2546 else
2547 value = eval_unary_shell(expr, shell)
2548 end if
2549 end function
2550
2551 recursive function eval_unary_shell(expr, shell) result(value)
2552 character(len=*), intent(in) :: expr
2553 type(shell_state_t), intent(inout) :: shell
2554 integer(kind=8) :: value, current_val
2555 character(len=512) :: rest, var_name, var_value_str, trimmed_expr
2556 character(len=:), allocatable :: temp_value
2557 integer :: iostat
2558
2559 if (len_trim(expr) == 0) then; value = 0; return; end if
2560
2561 ! Trim leading/trailing whitespace for all checks
2562 trimmed_expr = trim(adjustl(expr))
2563
2564 ! Pre-increment: ++x (only if followed by a variable name, not a number)
2565 if (len_trim(trimmed_expr) > 2 .and. trimmed_expr(1:2) == '++') then
2566 var_name = trim(adjustl(trimmed_expr(3:)))
2567 ! Check if it starts with a letter or underscore (variable name)
2568 ! If it starts with a digit, it's double unary plus, not increment
2569 if (len_trim(var_name) > 0) then
2570 if ((var_name(1:1) >= 'a' .and. var_name(1:1) <= 'z') .or. &
2571 (var_name(1:1) >= 'A' .and. var_name(1:1) <= 'Z') .or. &
2572 var_name(1:1) == '_') then
2573 ! Get current value
2574 temp_value = get_shell_variable(shell, trim(var_name))
2575 if (len_trim(temp_value) > 0) then
2576 read(temp_value, *, iostat=iostat) current_val
2577 if (iostat /= 0) current_val = 0
2578 else
2579 current_val = 0
2580 end if
2581 ! Increment
2582 value = current_val + 1
2583 ! Set variable
2584 write(var_value_str, '(I0)') value
2585 call set_shell_variable(shell, trim(var_name), trim(var_value_str))
2586 return
2587 end if
2588 ! Otherwise fall through to unary plus handling
2589 end if
2590 end if
2591
2592 ! Pre-decrement: --x (only if followed by a variable name, not a number)
2593 if (len_trim(trimmed_expr) > 2 .and. trimmed_expr(1:2) == '--') then
2594 var_name = trim(adjustl(trimmed_expr(3:)))
2595 ! Check if it starts with a letter or underscore (variable name)
2596 ! If it starts with a digit, it's double unary minus, not decrement
2597 if (len_trim(var_name) > 0) then
2598 if ((var_name(1:1) >= 'a' .and. var_name(1:1) <= 'z') .or. &
2599 (var_name(1:1) >= 'A' .and. var_name(1:1) <= 'Z') .or. &
2600 var_name(1:1) == '_') then
2601 ! Get current value
2602 temp_value = get_shell_variable(shell, trim(var_name))
2603 if (len_trim(temp_value) > 0) then
2604 read(temp_value, *, iostat=iostat) current_val
2605 if (iostat /= 0) current_val = 0
2606 else
2607 current_val = 0
2608 end if
2609 ! Decrement
2610 value = current_val - 1
2611 ! Set variable
2612 write(var_value_str, '(I0)') value
2613 call set_shell_variable(shell, trim(var_name), trim(var_value_str))
2614 return
2615 end if
2616 ! Otherwise fall through to unary minus handling
2617 end if
2618 end if
2619
2620 if (trimmed_expr(1:1) == '!') then
2621 rest = adjustl(trimmed_expr(2:))
2622 value = eval_unary_shell(rest, shell)
2623 if (value == 0) then; value = 1; else; value = 0; end if
2624 return
2625 end if
2626
2627 ! Bitwise NOT (~)
2628 if (trimmed_expr(1:1) == '~') then
2629 rest = adjustl(trimmed_expr(2:))
2630 value = eval_unary_shell(rest, shell)
2631 ! Bitwise NOT in two's complement: ~n = -(n + 1)
2632 value = -(value + 1)
2633 return
2634 end if
2635
2636 if (trimmed_expr(1:1) == '-' .and. len_trim(trimmed_expr) > 1) then
2637 rest = adjustl(trimmed_expr(2:))
2638 value = -eval_unary_shell(rest, shell)
2639 return
2640 end if
2641
2642 if (trimmed_expr(1:1) == '+' .and. len_trim(trimmed_expr) > 1) then
2643 rest = adjustl(trimmed_expr(2:))
2644 value = eval_unary_shell(rest, shell)
2645 return
2646 end if
2647
2648 value = eval_primary_shell(expr, shell)
2649 end function
2650
2651 function eval_primary_shell(expr, shell) result(value)
2652 character(len=*), intent(in) :: expr
2653 type(shell_state_t), intent(inout) :: shell
2654 integer(kind=8) :: value, new_val
2655 character(len=512) :: inner_expr, temp_expr, var_name, var_value_str
2656 character(len=:), allocatable :: var_value
2657 integer :: iostat, paren_end, expr_len
2658
2659 if (len_trim(expr) == 0) then; value = 0; return; end if
2660
2661 expr_len = len_trim(expr)
2662
2663 ! Check for post-increment: x++ (only if x is a valid variable name)
2664 if (expr_len > 2 .and. expr(expr_len-1:expr_len) == '++') then
2665 var_name = trim(adjustl(expr(:expr_len-2)))
2666 ! Only treat as post-increment if var_name is a valid identifier (no operators)
2667 if (is_valid_identifier(var_name)) then
2668 ! Get current value
2669 var_value = get_shell_variable(shell, trim(var_name))
2670 if (len_trim(var_value) > 0) then
2671 read(var_value, *, iostat=iostat) value
2672 if (iostat /= 0) value = 0
2673 else
2674 value = 0
2675 end if
2676 ! Increment and set
2677 new_val = value + 1
2678 write(var_value_str, '(I0)') new_val
2679 call set_shell_variable(shell, trim(var_name), trim(var_value_str))
2680 ! Return old value
2681 return
2682 end if
2683 end if
2684
2685 ! Check for post-decrement: x-- (only if x is a valid variable name)
2686 if (expr_len > 2 .and. expr(expr_len-1:expr_len) == '--') then
2687 var_name = trim(adjustl(expr(:expr_len-2)))
2688 ! Only treat as post-decrement if var_name is a valid identifier (no operators)
2689 if (is_valid_identifier(var_name)) then
2690 ! Get current value
2691 var_value = get_shell_variable(shell, trim(var_name))
2692 if (len_trim(var_value) > 0) then
2693 read(var_value, *, iostat=iostat) value
2694 if (iostat /= 0) value = 0
2695 else
2696 value = 0
2697 end if
2698 ! Decrement and set
2699 new_val = value - 1
2700 write(var_value_str, '(I0)') new_val
2701 call set_shell_variable(shell, trim(var_name), trim(var_value_str))
2702 ! Return old value
2703 return
2704 end if
2705 end if
2706
2707 ! Handle parentheses
2708 if (expr(1:1) == '(') then
2709 paren_end = find_matching_paren(expr, 1)
2710 if (paren_end > 1) then
2711 inner_expr = expr(2:paren_end-1)
2712 value = eval_expression_shell(trim(adjustl(inner_expr)), shell)
2713 return
2714 end if
2715 end if
2716
2717 ! Try to parse as number (with octal/hex support)
2718 temp_expr = trim(adjustl(expr))
2719 value = parse_arithmetic_number(temp_expr, iostat)
2720 if (iostat == 0) return
2721
2722 ! Check if it's a valid identifier before treating as variable
2723 if (.not. is_valid_identifier(trim(adjustl(expr)))) then
2724 ! Not a number and not a valid identifier - syntax error
2725 arithmetic_error = .true.
2726 arithmetic_error_msg = 'syntax error in expression (error token is "' // trim(adjustl(expr)) // '")'
2727 value = 0
2728 return
2729 end if
2730
2731 ! Resolve as variable (valid identifier)
2732 var_value = get_shell_variable(shell, trim(adjustl(expr)))
2733 if (len_trim(var_value) > 0) then
2734 value = parse_arithmetic_number(trim(var_value), iostat)
2735 if (iostat == 0) return
2736 ! Variable exists but is not numeric - try recursive evaluation
2737 value = eval_expression_shell(trim(var_value), shell)
2738 return
2739 end if
2740
2741 ! Valid identifier but variable not found or empty - return 0
2742 value = 0
2743 end function
2744
2745 ! Helper: Find matching closing parenthesis
2746 function find_matching_paren(expr, start_pos) result(end_pos)
2747 character(len=*), intent(in) :: expr
2748 integer, intent(in) :: start_pos
2749 integer :: end_pos, depth, i
2750
2751 depth = 0
2752 do i = start_pos, len_trim(expr)
2753 if (expr(i:i) == '(') then
2754 depth = depth + 1
2755 else if (expr(i:i) == ')') then
2756 depth = depth - 1
2757 if (depth == 0) then
2758 end_pos = i
2759 return
2760 end if
2761 end if
2762 end do
2763 end_pos = 0
2764 end function
2765
2766 ! Helper: Find operator (2-char) outside parentheses
2767 function find_operator(expr, op) result(pos)
2768 character(len=*), intent(in) :: expr, op
2769 integer :: pos, i, depth
2770
2771 depth = 0
2772 do i = 1, len_trim(expr) - len(op) + 1
2773 if (expr(i:i) == '(') then
2774 depth = depth + 1
2775 else if (expr(i:i) == ')') then
2776 depth = depth - 1
2777 else if (depth == 0 .and. expr(i:i+len(op)-1) == op) then
2778 pos = i
2779 return
2780 end if
2781 end do
2782 pos = 0
2783 end function
2784
2785 ! Helper: Find single-char operator outside parentheses
2786 function find_single_operator(expr, op) result(pos)
2787 character(len=*), intent(in) :: expr
2788 character, intent(in) :: op
2789 integer :: pos, i, depth
2790
2791 depth = 0
2792 do i = 1, len_trim(expr)
2793 if (expr(i:i) == '(') then
2794 depth = depth + 1
2795 else if (expr(i:i) == ')') then
2796 depth = depth - 1
2797 else if (depth == 0 .and. expr(i:i) == op) then
2798 ! Make sure it's not part of a 2-char operator
2799 if (i < len_trim(expr)) then
2800 if (op == '&' .and. expr(i+1:i+1) == '&') cycle
2801 if (op == '|' .and. expr(i+1:i+1) == '|') cycle
2802 if (op == '=' .and. expr(i+1:i+1) == '=') cycle
2803 if (op == '!' .and. expr(i+1:i+1) == '=') cycle
2804 if (op == '<' .and. (expr(i+1:i+1) == '=' .or. expr(i+1:i+1) == '<')) cycle
2805 if (op == '>' .and. (expr(i+1:i+1) == '=' .or. expr(i+1:i+1) == '>')) cycle
2806 if (op == '*' .and. expr(i+1:i+1) == '*') cycle
2807 end if
2808 if (i > 1) then
2809 if (op == '=' .and. (expr(i-1:i-1) == '=' .or. expr(i-1:i-1) == '!' .or. &
2810 expr(i-1:i-1) == '<' .or. expr(i-1:i-1) == '>')) cycle
2811 ! Also check if < or > is the second char of << or >>
2812 if (op == '<' .and. expr(i-1:i-1) == '<') cycle
2813 if (op == '>' .and. expr(i-1:i-1) == '>') cycle
2814 end if
2815 pos = i
2816 return
2817 end if
2818 end do
2819 pos = 0
2820 end function
2821
2822 ! Helper: Find rightmost +/- at depth 0
2823 function find_rightmost_additive(expr) result(pos)
2824 character(len=*), intent(in) :: expr
2825 integer :: pos, i, depth, j
2826 character(len=1) :: prev_ch
2827 logical :: is_unary
2828
2829 pos = 0
2830 depth = 0
2831 do i = len_trim(expr), 1, -1
2832 if (expr(i:i) == ')') then
2833 depth = depth + 1
2834 else if (expr(i:i) == '(') then
2835 depth = depth - 1
2836 else if (depth == 0 .and. (expr(i:i) == '+' .or. expr(i:i) == '-')) then
2837 ! Skip if it's part of unary operator at start
2838 if (i == 1) cycle
2839 ! Skip if this is part of ++ or -- (increment/decrement operators)
2840 ! Check for both pre-increment (++x) and post-increment (x++)
2841 if (i < len_trim(expr)) then
2842 if (expr(i:i) == '+' .and. expr(i+1:i+1) == '+') then
2843 ! Skip for pre-increment: ++x (followed by letter/underscore)
2844 if (i+2 <= len_trim(expr)) then
2845 if ((expr(i+2:i+2) >= 'a' .and. expr(i+2:i+2) <= 'z') .or. &
2846 (expr(i+2:i+2) >= 'A' .and. expr(i+2:i+2) <= 'Z') .or. &
2847 expr(i+2:i+2) == '_') cycle
2848 end if
2849 end if
2850 if (expr(i:i) == '-' .and. expr(i+1:i+1) == '-') then
2851 ! Skip for pre-decrement: --x (followed by letter/underscore)
2852 if (i+2 <= len_trim(expr)) then
2853 if ((expr(i+2:i+2) >= 'a' .and. expr(i+2:i+2) <= 'z') .or. &
2854 (expr(i+2:i+2) >= 'A' .and. expr(i+2:i+2) <= 'Z') .or. &
2855 expr(i+2:i+2) == '_') cycle
2856 end if
2857 end if
2858 end if
2859 ! Skip for post-increment/decrement: x++ or x-- (preceded by valid identifier)
2860 if (i > 1) then
2861 if ((expr(i:i) == '+' .and. expr(i+1:i+1) == '+') .or. &
2862 (expr(i:i) == '-' .and. expr(i+1:i+1) == '-')) then
2863 ! Extract the full token before the operator
2864 ! Find the start of the identifier by scanning backwards
2865 j = i - 1
2866 do while (j > 1)
2867 if (.not. ((expr(j-1:j-1) >= 'a' .and. expr(j-1:j-1) <= 'z') .or. &
2868 (expr(j-1:j-1) >= 'A' .and. expr(j-1:j-1) <= 'Z') .or. &
2869 (expr(j-1:j-1) >= '0' .and. expr(j-1:j-1) <= '9') .or. &
2870 expr(j-1:j-1) == '_')) exit
2871 j = j - 1
2872 end do
2873 ! Now j points to the start of the potential identifier
2874 ! Check if it's a valid identifier (not just digits, must start with letter/_)
2875 if (is_valid_identifier(expr(j:i-1))) cycle
2876 end if
2877 end if
2878 ! Skip if previous non-space char makes this unary
2879 ! Find previous non-space character
2880 prev_ch = ' '
2881 do j = i-1, 1, -1
2882 if (expr(j:j) /= ' ') then
2883 prev_ch = expr(j:j)
2884 exit
2885 end if
2886 end do
2887 ! If no non-space char found (i.e., at start), it's unary
2888 is_unary = (prev_ch == ' ')
2889 ! Check if previous char makes this unary
2890 if (.not. is_unary) then
2891 is_unary = (prev_ch == '(' .or. prev_ch == '+' .or. &
2892 prev_ch == '-' .or. prev_ch == '*' .or. &
2893 prev_ch == '/' .or. prev_ch == '%' .or. &
2894 prev_ch == '=' .or. prev_ch == '!' .or. &
2895 prev_ch == '<' .or. prev_ch == '>' .or. &
2896 prev_ch == '&' .or. prev_ch == '|' .or. &
2897 prev_ch == '^' .or. prev_ch == ',')
2898 end if
2899 if (is_unary) cycle
2900 pos = i
2901 return
2902 end if
2903 end do
2904 end function
2905
2906 ! Helper: Find rightmost *,/,% at depth 0
2907 function find_rightmost_multiplicative(expr, op) result(pos)
2908 character(len=*), intent(in) :: expr
2909 character, intent(out) :: op
2910 integer :: pos, i, depth
2911
2912 pos = 0
2913 depth = 0
2914 do i = len_trim(expr), 1, -1
2915 if (expr(i:i) == ')') then
2916 depth = depth + 1
2917 else if (expr(i:i) == '(') then
2918 depth = depth - 1
2919 else if (depth == 0) then
2920 if (expr(i:i) == '*' .or. expr(i:i) == '/' .or. expr(i:i) == '%') then
2921 ! Skip ** (power operator)
2922 if (expr(i:i) == '*' .and. i < len_trim(expr) .and. expr(i+1:i+1) == '*') cycle
2923 if (expr(i:i) == '*' .and. i > 1 .and. expr(i-1:i-1) == '*') cycle
2924 pos = i
2925 op = expr(i:i)
2926 return
2927 end if
2928 end if
2929 end do
2930 op = ' '
2931 end function
2932
2933 ! Enhanced variable expansion with array and parameter support
2934 subroutine enhanced_expand_variables(input, expanded, shell)
2935 character(len=*), intent(in) :: input
2936 character(len=:), allocatable, intent(out) :: expanded
2937 type(shell_state_t), intent(inout) :: shell
2938 #ifdef USE_C_STRINGS
2939 ! flang-new path: C-backed buffer avoids allocatable churn
2940 type(c_ptr) :: rbuf
2941 integer :: i, start_pos, bracket_count, rc, vlen
2942 integer(c_size_t) :: buf_len, copied
2943 character(len=256) :: var_expr
2944 character(len=:), allocatable :: var_value
2945 logical :: in_single_quote, in_double_quote
2946
2947 rbuf = c_buf_create(int(len(input) * 2 + 256, c_size_t))
2948
2949 i = 1
2950 in_single_quote = .false.
2951 in_double_quote = .false.
2952
2953 do while (i <= len_trim(input))
2954 ! Handle quote characters
2955 if (input(i:i) == "'" .and. .not. in_double_quote) then
2956 in_single_quote = .not. in_single_quote
2957 rc = c_buf_append_char(rbuf, input(i:i))
2958 i = i + 1
2959 cycle
2960 else if (input(i:i) == '"' .and. .not. in_single_quote) then
2961 if (i > 1 .and. input(i-1:i-1) == '\') then
2962 ! Escaped double quote — overwrite the trailing backslash
2963 ! The backslash was already appended; we need to replace it.
2964 ! For simplicity, just append the quote (the backslash is already there)
2965 ! This matches original behavior: result(len_trim(result):len_trim(result)) = '"'
2966 ! We approximate by appending — the original code overwrote the last char.
2967 ! TODO: if this causes issues, add c_buf_set_last_char
2968 buf_len = c_buf_length(rbuf)
2969 if (buf_len > 0) then
2970 ! Overwrite last char via the C buffer's data directly
2971 rc = c_buf_append_char(rbuf, '"')
2972 ! Actually, we need to back up one position. Use a workaround:
2973 ! The original code did result(len_trim:len_trim) = '"' which overwrites
2974 ! the backslash. We can't easily do that with append. But the original
2975 ! behavior is: backslash was the last char appended, replace it with ".
2976 ! Since we can't easily do a set-last-char, just append " and let the
2977 ! shell's quote removal phase handle the \" pair.
2978 else
2979 rc = c_buf_append_char(rbuf, '"')
2980 end if
2981 i = i + 1
2982 cycle
2983 else
2984 in_double_quote = .not. in_double_quote
2985 rc = c_buf_append_char(rbuf, input(i:i))
2986 i = i + 1
2987 cycle
2988 end if
2989 end if
2990
2991 ! Skip all expansions inside single quotes
2992 if (in_single_quote) then
2993 rc = c_buf_append_char(rbuf, input(i:i))
2994 i = i + 1
2995 cycle
2996 end if
2997
2998 ! Now handle expansions (only active outside single quotes)
2999 if (i < len_trim(input) - 2 .and. input(i:i+2) == '$((') then
3000 ! Arithmetic expansion $((expr))
3001 start_pos = i
3002 bracket_count = 2
3003 i = i + 3
3004
3005 do while (i <= len_trim(input) .and. bracket_count > 0)
3006 if (input(i:i) == '(') bracket_count = bracket_count + 1
3007 if (input(i:i) == ')') bracket_count = bracket_count - 1
3008 i = i + 1
3009 end do
3010
3011 if (bracket_count == 0) then
3012 var_expr = input(start_pos:i-1)
3013 var_value = arithmetic_expansion_shell(var_expr, shell)
3014 vlen = len_trim(var_value)
3015 if (vlen > 0) rc = c_buf_append_chars(rbuf, var_value, int(vlen, c_size_t))
3016 end if
3017
3018 else if (i < len_trim(input) - 1 .and. input(i:i+1) == '$(' .and. &
3019 (i >= len_trim(input) - 2 .or. input(i:i+2) /= '$((')) then
3020 ! Command substitution $(command)
3021 start_pos = i
3022 bracket_count = 1
3023 i = i + 2
3024
3025 ! Find matching ) with quote awareness
3026 do while (i <= len_trim(input) .and. bracket_count > 0)
3027 if (input(i:i) == '"') then
3028 i = i + 1
3029 do while (i <= len_trim(input))
3030 if (input(i:i) == '\' .and. i < len_trim(input)) then
3031 i = i + 2
3032 else if (input(i:i) == '"') then
3033 i = i + 1
3034 exit
3035 else
3036 i = i + 1
3037 end if
3038 end do
3039 else if (input(i:i) == "'") then
3040 i = i + 1
3041 do while (i <= len_trim(input) .and. input(i:i) /= "'")
3042 i = i + 1
3043 end do
3044 if (i <= len_trim(input)) i = i + 1
3045 else if (input(i:i) == '(') then
3046 bracket_count = bracket_count + 1
3047 i = i + 1
3048 else if (input(i:i) == ')') then
3049 bracket_count = bracket_count - 1
3050 i = i + 1
3051 else
3052 i = i + 1
3053 end if
3054 end do
3055
3056 if (bracket_count == 0) then
3057 var_expr = input(start_pos+2:i-2)
3058 shell%in_command_substitution = .true.
3059 call execute_command_and_capture(shell, trim(var_expr), var_value)
3060 shell%in_command_substitution = .false.
3061 vlen = len_trim(var_value)
3062 if (vlen > 0) rc = c_buf_append_chars(rbuf, var_value, int(vlen, c_size_t))
3063 end if
3064
3065 else if (i < len_trim(input) - 1 .and. input(i:i+1) == '${') then
3066 ! Parameter expansion ${var}
3067 start_pos = i
3068 bracket_count = 1
3069 i = i + 2
3070
3071 do while (i <= len_trim(input) .and. bracket_count > 0)
3072 if (input(i:i) == '{') bracket_count = bracket_count + 1
3073 if (input(i:i) == '}') bracket_count = bracket_count - 1
3074 i = i + 1
3075 end do
3076
3077 if (bracket_count == 0) then
3078 var_expr = input(start_pos:i-1)
3079 call parameter_expansion_to_buf(shell, var_expr, rbuf)
3080 end if
3081
3082 else if (input(i:i) == '$') then
3083 if (i > 1 .and. input(i-1:i-1) == '\') then
3084 rc = c_buf_append_char(rbuf, '$')
3085 i = i + 1
3086 cycle
3087 else
3088 start_pos = i + 1
3089 i = i + 1
3090
3091 if (i <= len_trim(input)) then
3092 if (index('$!?0-_#*@', input(i:i)) > 0) then
3093 var_expr = input(i:i)
3094 var_value = get_shell_variable(shell, trim(var_expr))
3095 vlen = len_trim(var_value)
3096 if (vlen > 0) rc = c_buf_append_chars(rbuf, var_value, int(vlen, c_size_t))
3097 i = i + 1
3098 else if (is_alnum(input(i:i)) .or. input(i:i) == '_') then
3099 do while (i <= len_trim(input) .and. (is_alnum(input(i:i)) .or. input(i:i) == '_'))
3100 i = i + 1
3101 end do
3102 var_expr = input(start_pos:i-1)
3103 var_value = get_shell_variable(shell, trim(var_expr))
3104 vlen = len_trim(var_value)
3105 if (vlen > 0) rc = c_buf_append_chars(rbuf, var_value, int(vlen, c_size_t))
3106 else
3107 rc = c_buf_append_char(rbuf, '$')
3108 end if
3109 else
3110 rc = c_buf_append_char(rbuf, '$')
3111 end if
3112 end if
3113
3114 else
3115 rc = c_buf_append_char(rbuf, input(i:i))
3116 i = i + 1
3117 end if
3118 end do
3119
3120 ! Single extraction: C buffer -> Fortran allocatable (one allocation)
3121 buf_len = c_buf_length(rbuf)
3122 if (buf_len > 0) then
3123 allocate(character(len=int(buf_len)) :: expanded)
3124 copied = c_buf_to_fortran(rbuf, expanded, buf_len)
3125 else
3126 expanded = ''
3127 end if
3128 call c_buf_destroy(rbuf)
3129 #else
3130 ! gfortran path: native Fortran allocatable (safe on x86_64)
3131 character(len=:), allocatable :: result
3132 integer :: i, start_pos, bracket_count, result_capacity, result_pos
3133 character(len=256) :: var_expr
3134 character(len=:), allocatable :: var_value
3135 logical :: in_single_quote, in_double_quote
3136
3137 result_capacity = len(input) * 2 + 256
3138 allocate(character(len=result_capacity) :: result)
3139 result = ''
3140 result_pos = 0
3141 i = 1
3142 in_single_quote = .false.
3143 in_double_quote = .false.
3144
3145 do while (i <= len_trim(input))
3146 if (input(i:i) == "'" .and. .not. in_double_quote) then
3147 in_single_quote = .not. in_single_quote
3148 result = trim(result) // input(i:i)
3149 i = i + 1
3150 cycle
3151 else if (input(i:i) == '"' .and. .not. in_single_quote) then
3152 if (i > 1 .and. input(i-1:i-1) == '\') then
3153 result(len_trim(result):len_trim(result)) = '"'
3154 i = i + 1
3155 cycle
3156 else
3157 in_double_quote = .not. in_double_quote
3158 result = trim(result) // input(i:i)
3159 i = i + 1
3160 cycle
3161 end if
3162 end if
3163
3164 if (in_single_quote) then
3165 result = trim(result) // input(i:i)
3166 i = i + 1
3167 cycle
3168 end if
3169
3170 if (i < len_trim(input) - 2 .and. input(i:i+2) == '$((') then
3171 start_pos = i
3172 bracket_count = 2
3173 i = i + 3
3174 do while (i <= len_trim(input) .and. bracket_count > 0)
3175 if (input(i:i) == '(') bracket_count = bracket_count + 1
3176 if (input(i:i) == ')') bracket_count = bracket_count - 1
3177 i = i + 1
3178 end do
3179 if (bracket_count == 0) then
3180 var_expr = input(start_pos:i-1)
3181 var_value = arithmetic_expansion_shell(var_expr, shell)
3182 result = trim(result) // trim(var_value)
3183 end if
3184 else if (i < len_trim(input) - 1 .and. input(i:i+1) == '$(' .and. &
3185 (i >= len_trim(input) - 2 .or. input(i:i+2) /= '$((')) then
3186 start_pos = i
3187 bracket_count = 1
3188 i = i + 2
3189 do while (i <= len_trim(input) .and. bracket_count > 0)
3190 if (input(i:i) == '"') then
3191 i = i + 1
3192 do while (i <= len_trim(input))
3193 if (input(i:i) == '\' .and. i < len_trim(input)) then
3194 i = i + 2
3195 else if (input(i:i) == '"') then
3196 i = i + 1
3197 exit
3198 else
3199 i = i + 1
3200 end if
3201 end do
3202 else if (input(i:i) == "'") then
3203 i = i + 1
3204 do while (i <= len_trim(input) .and. input(i:i) /= "'")
3205 i = i + 1
3206 end do
3207 if (i <= len_trim(input)) i = i + 1
3208 else if (input(i:i) == '(') then
3209 bracket_count = bracket_count + 1
3210 i = i + 1
3211 else if (input(i:i) == ')') then
3212 bracket_count = bracket_count - 1
3213 i = i + 1
3214 else
3215 i = i + 1
3216 end if
3217 end do
3218 if (bracket_count == 0) then
3219 var_expr = input(start_pos+2:i-2)
3220 shell%in_command_substitution = .true.
3221 call execute_command_and_capture(shell, trim(var_expr), var_value)
3222 shell%in_command_substitution = .false.
3223 result = trim(result) // trim(var_value)
3224 end if
3225 else if (i < len_trim(input) - 1 .and. input(i:i+1) == '${') then
3226 start_pos = i
3227 bracket_count = 1
3228 i = i + 2
3229 do while (i <= len_trim(input) .and. bracket_count > 0)
3230 if (input(i:i) == '{') bracket_count = bracket_count + 1
3231 if (input(i:i) == '}') bracket_count = bracket_count - 1
3232 i = i + 1
3233 end do
3234 if (bracket_count == 0) then
3235 var_expr = input(start_pos:i-1)
3236 var_value = parameter_expansion(shell, var_expr)
3237 result = trim(result) // trim(var_value)
3238 end if
3239 else if (input(i:i) == '$') then
3240 if (i > 1 .and. input(i-1:i-1) == '\') then
3241 result = trim(result) // '$'
3242 i = i + 1
3243 cycle
3244 else
3245 start_pos = i + 1
3246 i = i + 1
3247 if (i <= len_trim(input)) then
3248 if (index('$!?0-_#*@', input(i:i)) > 0) then
3249 var_expr = input(i:i)
3250 var_value = get_shell_variable(shell, trim(var_expr))
3251 result = trim(result) // trim(var_value)
3252 i = i + 1
3253 else if (is_alnum(input(i:i)) .or. input(i:i) == '_') then
3254 do while (i <= len_trim(input) .and. (is_alnum(input(i:i)) .or. input(i:i) == '_'))
3255 i = i + 1
3256 end do
3257 var_expr = input(start_pos:i-1)
3258 var_value = get_shell_variable(shell, trim(var_expr))
3259 result = trim(result) // trim(var_value)
3260 else
3261 result = trim(result) // '$'
3262 end if
3263 else
3264 result = trim(result) // '$'
3265 end if
3266 end if
3267 else
3268 result = trim(result) // input(i:i)
3269 i = i + 1
3270 end if
3271 end do
3272 expanded = trim(result)
3273 #endif
3274 end subroutine
3275
3276 #ifdef USE_C_STRINGS
3277 ! Run parameter_expansion and append result to a C buffer.
3278 ! For the common case (result < 2KB), uses the standard allocatable path.
3279 ! For the pattern-replace path that can produce large results, writes
3280 ! directly to the C buffer without any large Fortran allocatable.
3281 subroutine parameter_expansion_to_buf(shell, expression, dest_buf)
3282 type(shell_state_t), intent(inout) :: shell
3283 character(len=*), intent(in) :: expression
3284 type(c_ptr), intent(in) :: dest_buf
3285 character(len=:), allocatable :: tmp
3286 integer :: tlen, rc
3287 ! For large pattern-replace: bypass the allocatable return entirely
3288 type(c_ptr) :: var_buf, result_ptr
3289 character(kind=c_char), pointer :: raw(:)
3290 integer :: vlen, pat_len, repl_len, result_len, slash_pos, i
3291 integer(c_int) :: c_replace_all
3292 integer(c_size_t) :: copied
3293 character(len=256) :: var_name, operation
3294 character(len=:), allocatable :: pattern
3295 character(len=256) :: replacement
3296 logical :: replace_all
3297
3298 ! Quick check: is this a pattern-replace expression? (${var/pat/repl} or ${var//pat/repl})
3299 if (len_trim(expression) >= 4) then
3300 var_name = expression(3:len_trim(expression)-1)
3301 slash_pos = index(var_name, '/')
3302 if (slash_pos > 0) then
3303 i = index(var_name(slash_pos+1:), '/')
3304 if (i > 0) then
3305 ! This IS a pattern replace. Check for // (global)
3306 i = slash_pos + i
3307 operation = var_name(:slash_pos-1)
3308 pattern = var_name(slash_pos+1:i-1)
3309 replacement = var_name(i+1:)
3310
3311 if (slash_pos > 1 .and. var_name(slash_pos-1:slash_pos-1) == '/') then
3312 replace_all = .true.
3313 operation = var_name(:slash_pos-2)
3314 else
3315 replace_all = .false.
3316 end if
3317
3318 ! Skip anchor patterns (handled fine by the standard path since results are similar size)
3319 if (len_trim(pattern) > 0 .and. (pattern(1:1) == '#' .or. pattern(1:1) == '%')) then
3320 ! Fall through to standard path below
3321 else
3322 ! === FULL C-BUFFER PATH: var_value → C replace → dest_buf ===
3323 var_buf = c_buf_create(256_c_size_t)
3324 call get_shell_variable_to_cbuf(shell, trim(operation), var_buf)
3325 vlen = int(c_buf_length(var_buf))
3326 pat_len = len_trim(pattern)
3327 repl_len = len_trim(replacement)
3328
3329 if (pat_len == 0 .or. vlen == 0) then
3330 ! No pattern or empty var: append var as-is
3331 if (vlen > 0) then
3332 ! Copy var_buf contents to dest_buf
3333 allocate(character(len=vlen) :: tmp)
3334 copied = c_buf_to_fortran(var_buf, tmp, int(vlen, c_size_t))
3335 rc = c_buf_append_chars(dest_buf, tmp, int(vlen, c_size_t))
3336 deallocate(tmp)
3337 end if
3338 call c_buf_destroy(var_buf)
3339 return
3340 end if
3341
3342 if (replace_all) then
3343 c_replace_all = 1_c_int
3344 else
3345 c_replace_all = 0_c_int
3346 end if
3347
3348 ! C reads directly from var_buf, produces malloc'd result
3349 result_len = c_buf_pattern_replace(var_buf, pattern, int(pat_len, c_int), &
3350 replacement, int(repl_len, c_int), &
3351 c_replace_all, result_ptr)
3352 call c_buf_destroy(var_buf)
3353
3354 ! Append C result directly to dest_buf — ZERO large Fortran allocatables
3355 if (result_len > 0) then
3356 call c_f_pointer(result_ptr, raw, [result_len])
3357 rc = c_buf_append_chars(dest_buf, raw, int(result_len, c_size_t))
3358 end if
3359 call c_free_string(result_ptr)
3360 return
3361 end if
3362 end if
3363 end if
3364 end if
3365
3366 ! Standard path: call parameter_expansion, extract result
3367 tmp = parameter_expansion(shell, expression)
3368 tlen = len_trim(tmp)
3369 if (tlen > 0) rc = c_buf_append_chars(dest_buf, tmp, int(tlen, c_size_t))
3370 end subroutine
3371
3372 ! Get a shell variable value into a C buffer — zero allocatable intermediaries.
3373 ! Calls get_shell_variable_to_cbuf which copies directly from variable storage
3374 ! into the C buffer via memcpy, bypassing flang-new's allocatable return path.
3375 subroutine get_var_to_buf(shell, name, dest_buf)
3376 type(shell_state_t), intent(in) :: shell
3377 character(len=*), intent(in) :: name
3378 type(c_ptr), intent(in) :: dest_buf
3379
3380 call get_shell_variable_to_cbuf(shell, name, dest_buf)
3381 end subroutine
3382 #endif
3383
3384 function is_alnum(c) result(is_valid)
3385 character, intent(in) :: c
3386 logical :: is_valid
3387
3388 is_valid = (c >= 'a' .and. c <= 'z') .or. &
3389 (c >= 'A' .and. c <= 'Z') .or. &
3390 (c >= '0' .and. c <= '9')
3391 end function
3392
3393 ! Check if a string is a valid shell variable identifier
3394 function is_valid_identifier(str) result(is_valid)
3395 character(len=*), intent(in) :: str
3396 logical :: is_valid
3397 integer :: i
3398 character(len=1) :: c
3399
3400 is_valid = .false.
3401 if (len_trim(str) == 0) return
3402
3403 ! First character must be letter or underscore
3404 c = str(1:1)
3405 if (.not. ((c >= 'a' .and. c <= 'z') .or. &
3406 (c >= 'A' .and. c <= 'Z') .or. &
3407 c == '_')) return
3408
3409 ! Remaining characters must be letters, digits, or underscores
3410 do i = 2, len_trim(str)
3411 c = str(i:i)
3412 if (.not. ((c >= 'a' .and. c <= 'z') .or. &
3413 (c >= 'A' .and. c <= 'Z') .or. &
3414 (c >= '0' .and. c <= '9') .or. &
3415 c == '_')) return
3416 end do
3417
3418 is_valid = .true.
3419 end function is_valid_identifier
3420
3421 ! Field splitting based on IFS
3422 subroutine field_split(input, ifs_chars, fields, field_count)
3423 character(len=*), intent(in) :: input, ifs_chars
3424 character(len=*), intent(out) :: fields(:)
3425 integer, intent(out) :: field_count
3426
3427 integer :: i, field_idx, input_len
3428 logical :: prev_was_ifs, is_ifs_char, is_whitespace_ifs
3429 logical :: prev_was_nonws_ifs ! Previous was non-whitespace IFS
3430 character(len=:), allocatable :: current_field
3431 logical :: has_whitespace_ifs
3432
3433 field_count = 0
3434 field_idx = 1
3435 current_field = ''
3436 prev_was_ifs = .false.
3437 prev_was_nonws_ifs = .false.
3438
3439 ! Handle empty input
3440 input_len = len_trim(input)
3441 if (input_len == 0) then
3442 return
3443 end if
3444
3445 ! Check if IFS contains whitespace characters
3446 has_whitespace_ifs = (index(ifs_chars, ' ') > 0) .or. &
3447 (index(ifs_chars, char(9)) > 0) .or. &
3448 (index(ifs_chars, char(10)) > 0)
3449
3450 ! Special handling for first character
3451 is_ifs_char = index(ifs_chars, input(1:1)) > 0
3452 is_whitespace_ifs = is_ifs_char .and. &
3453 (input(1:1) == ' ' .or. input(1:1) == char(9) .or. input(1:1) == char(10))
3454
3455 ! Leading non-whitespace IFS characters create empty fields
3456 if (is_ifs_char .and. .not. is_whitespace_ifs) then
3457 ! Add empty field for leading delimiter
3458 if (field_idx <= size(fields)) then
3459 fields(field_idx) = ''
3460 field_idx = field_idx + 1
3461 field_count = field_count + 1
3462 end if
3463 prev_was_ifs = .true.
3464 prev_was_nonws_ifs = .true.
3465 else if (.not. is_ifs_char) then
3466 ! Start with non-IFS character
3467 current_field = input(1:1)
3468 prev_was_ifs = .false.
3469 prev_was_nonws_ifs = .false.
3470 else
3471 ! Leading whitespace IFS - skip
3472 prev_was_ifs = .true.
3473 prev_was_nonws_ifs = .false.
3474 end if
3475
3476 ! Process remaining characters
3477 do i = 2, input_len
3478 is_ifs_char = index(ifs_chars, input(i:i)) > 0
3479 is_whitespace_ifs = is_ifs_char .and. &
3480 (input(i:i) == ' ' .or. input(i:i) == char(9) .or. input(i:i) == char(10))
3481
3482 if (.not. is_ifs_char) then
3483 ! Non-IFS character
3484 if (prev_was_ifs .and. len_trim(current_field) > 0) then
3485 ! Save previous field
3486 if (field_idx <= size(fields)) then
3487 fields(field_idx) = current_field
3488 field_idx = field_idx + 1
3489 field_count = field_count + 1
3490 end if
3491 current_field = ''
3492 end if
3493 current_field = trim(current_field) // input(i:i)
3494 prev_was_ifs = .false.
3495 prev_was_nonws_ifs = .false.
3496 else
3497 ! IFS character
3498 if (len_trim(current_field) > 0) then
3499 ! Save current field when we have content
3500 if (field_idx <= size(fields)) then
3501 fields(field_idx) = current_field
3502 field_idx = field_idx + 1
3503 field_count = field_count + 1
3504 end if
3505 current_field = ''
3506 else if (.not. is_whitespace_ifs) then
3507 ! Non-whitespace IFS
3508 ! POSIX: Only create empty field for consecutive non-whitespace IFS
3509 ! without any whitespace between them. So "::" creates empty, but ": :" doesn't
3510 if (prev_was_nonws_ifs) then
3511 if (field_idx <= size(fields)) then
3512 fields(field_idx) = ''
3513 field_idx = field_idx + 1
3514 field_count = field_count + 1
3515 end if
3516 end if
3517 end if
3518
3519 ! Track state for next iteration
3520 prev_was_ifs = .true.
3521 if (.not. is_whitespace_ifs) then
3522 prev_was_nonws_ifs = .true.
3523 else
3524 ! Whitespace IFS resets the consecutive non-whitespace tracking
3525 prev_was_nonws_ifs = .false.
3526 end if
3527 end if
3528 end do
3529
3530 ! Handle last field
3531 ! Note: Trailing IFS delimiters should NOT create empty fields (POSIX)
3532 if (len_trim(current_field) > 0) then
3533 if (field_idx <= size(fields)) then
3534 fields(field_idx) = current_field
3535 field_count = field_count + 1
3536 end if
3537 end if
3538 end subroutine
3539
3540 ! Word splitting for unquoted variable expansions
3541 subroutine word_split(shell, input, words, word_count)
3542 type(shell_state_t), intent(inout) :: shell
3543 character(len=*), intent(in) :: input
3544 character(len=*), intent(out) :: words(:)
3545 integer, intent(out) :: word_count
3546
3547 character(len=256) :: ifs_to_use
3548 logical :: ifs_is_set
3549 integer :: ifs_actual_len
3550
3551 ! Check if IFS is explicitly set (even if empty)
3552 ifs_is_set = is_shell_variable_set(shell, 'IFS')
3553
3554 if (ifs_is_set) then
3555 ifs_to_use = shell%ifs
3556 ! Get the actual length of IFS from shell%ifs_len (preserves whitespace-only values)
3557 ifs_actual_len = shell%ifs_len
3558
3559 ! If IFS is set to empty string (length 0), no field splitting occurs
3560 ! But if IFS=" " (length 1), we should still split on that space
3561 if (ifs_actual_len == 0) then
3562 ! Empty IFS - return the entire input as a single field
3563 words(1) = input
3564 word_count = 1
3565 return
3566 end if
3567 ! Use the actual IFS length, not trimmed length
3568 call field_split(input, ifs_to_use(1:ifs_actual_len), words, word_count)
3569 else
3570 ! IFS not set - use default
3571 ifs_to_use = ' '//char(9)//char(10) ! space, tab, newline
3572 call field_split(input, trim(ifs_to_use), words, word_count)
3573 end if
3574
3575 ! POSIX: Remove null (empty) fields after field splitting
3576 ! Empty unquoted fields should be discarded
3577 call remove_null_fields(words, word_count)
3578 end subroutine
3579
3580 ! Remove null (empty) fields from word list
3581 ! According to POSIX, after field splitting, null fields should be removed
3582 subroutine remove_null_fields(words, word_count)
3583 character(len=*), intent(inout) :: words(:)
3584 integer, intent(inout) :: word_count
3585 integer :: i, j
3586
3587 j = 1
3588 do i = 1, word_count
3589 if (len_trim(words(i)) > 0) then
3590 if (i /= j) then
3591 words(j) = words(i)
3592 end if
3593 j = j + 1
3594 end if
3595 end do
3596 word_count = j - 1
3597 end subroutine
3598
3599 ! Quote removal - removes outer quotes from strings
3600 function remove_quotes(input) result(output)
3601 character(len=*), intent(in) :: input
3602 character(len=len(input)) :: output
3603 integer :: len_input
3604
3605 len_input = len_trim(input)
3606 output = input
3607
3608 if (len_input < 2) return
3609
3610 ! Remove single quotes
3611 if (input(1:1) == "'" .and. input(len_input:len_input) == "'") then
3612 if (len_input == 2) then
3613 output = ''
3614 else
3615 output = input(2:len_input-1)
3616 end if
3617 return
3618 end if
3619
3620 ! Remove double quotes (but preserve escaped characters inside)
3621 if (input(1:1) == '"' .and. input(len_input:len_input) == '"') then
3622 if (len_input == 2) then
3623 output = ''
3624 else
3625 output = input(2:len_input-1)
3626 ! TODO: Process escape sequences within double quotes
3627 end if
3628 return
3629 end if
3630 end function
3631
3632 ! Brace expansion - expands braces to multiple words
3633 ! Examples:
3634 ! {a,b,c} → a b c
3635 ! {1..5} → 1 2 3 4 5
3636 ! {a..e} → a b c d e
3637 ! {1..10..2} → 1 3 5 7 9
3638 ! file{1,2,3}.txt → file1.txt file2.txt file3.txt (prefix/suffix support)
3639 ! {A,B{1,2},C} → A B{1,2} C (respects nested braces)
3640 function expand_braces(word) result(expanded)
3641 character(*), intent(in) :: word
3642 character(len=:), allocatable :: expanded
3643 integer :: brace_start, brace_end, dot_pos, depth, pos
3644 character(len=:), allocatable :: prefix, brace_content, suffix, item
3645 character(len=:), allocatable :: result_buf
3646 integer :: i, start_val, end_val, step_val, current_val
3647 integer :: start_char, end_char, current_char
3648 integer :: last_pos, second_dot
3649 logical :: is_numeric, is_alpha, has_step, found_comma
3650 character(16) :: num_str
3651 character(len=:), allocatable :: start_str, end_str, step_str
3652 integer :: buf_pos, buf_cap, num_len, plen, slen, num_values, max_digits
3653
3654 expanded = word
3655
3656 ! Find opening brace that is NOT part of ${...} parameter expansion
3657 brace_start = 0
3658 pos = 1
3659 do while (pos <= len_trim(word))
3660 if (word(pos:pos) == '{') then
3661 ! Skip if preceded by $ (this is ${...} parameter expansion)
3662 if (pos > 1 .and. word(pos-1:pos-1) == '$') then
3663 ! Skip past matching closing brace
3664 depth = 1
3665 pos = pos + 1
3666 do while (pos <= len_trim(word) .and. depth > 0)
3667 if (word(pos:pos) == '{') depth = depth + 1
3668 if (word(pos:pos) == '}') depth = depth - 1
3669 pos = pos + 1
3670 end do
3671 cycle
3672 end if
3673 brace_start = pos
3674 exit
3675 end if
3676 pos = pos + 1
3677 end do
3678 if (brace_start == 0) return
3679
3680 ! Find MATCHING closing brace by counting depth (supports nested braces)
3681 depth = 0
3682 brace_end = 0
3683 do pos = brace_start, len_trim(word)
3684 if (word(pos:pos) == '{') then
3685 depth = depth + 1
3686 else if (word(pos:pos) == '}') then
3687 depth = depth - 1
3688 if (depth == 0) then
3689 brace_end = pos
3690 exit
3691 end if
3692 end if
3693 end do
3694
3695 if (brace_end == 0) return
3696
3697 ! Extract prefix, brace content, and suffix
3698 if (brace_start > 1) then
3699 prefix = word(1:brace_start-1)
3700 else
3701 prefix = ''
3702 end if
3703
3704 brace_content = word(brace_start+1:brace_end-1)
3705
3706 if (brace_end < len_trim(word)) then
3707 suffix = word(brace_end+1:)
3708 else
3709 suffix = ''
3710 end if
3711
3712 if (len_trim(brace_content) == 0) return
3713
3714 ! Check if it's a range expansion (contains ..)
3715 dot_pos = index(brace_content, '..')
3716 if (dot_pos > 0) then
3717 ! Range expansion: {start..end} or {start..end..step}
3718
3719 ! Extract start
3720 start_str = brace_content(1:dot_pos-1)
3721
3722 ! Check for step (second ..)
3723 second_dot = index(brace_content(dot_pos+2:), '..')
3724 has_step = (second_dot > 0)
3725
3726 if (has_step) then
3727 ! {start..end..step}
3728 second_dot = dot_pos + 1 + second_dot
3729 end_str = brace_content(dot_pos+2:second_dot-1)
3730 step_str = brace_content(second_dot+2:)
3731 read(step_str, *, iostat=i) step_val
3732 if (i /= 0) then
3733 step_val = 1
3734 end if
3735 else
3736 ! {start..end}
3737 end_str = brace_content(dot_pos+2:)
3738 step_val = 1
3739 end if
3740
3741 ! Check if numeric or alphabetic
3742 is_numeric = .false.
3743 is_alpha = .false.
3744
3745 read(start_str, *, iostat=i) start_val
3746 if (i == 0) then
3747 ! Numeric range
3748 read(end_str, *, iostat=i) end_val
3749 if (i == 0) then
3750 is_numeric = .true.
3751 end if
3752 end if
3753
3754 if (.not. is_numeric .and. len_trim(start_str) == 1 .and. len_trim(end_str) == 1) then
3755 ! Alphabetic range
3756 start_char = ichar(start_str(1:1))
3757 end_char = ichar(end_str(1:1))
3758 is_alpha = .true.
3759 end if
3760
3761 if (is_numeric) then
3762 ! Numeric range expansion — O(n) with pre-allocated buffer
3763 if (start_val <= end_val) then
3764 num_values = (end_val - start_val) / step_val + 1
3765 else
3766 num_values = (start_val - end_val) / step_val + 1
3767 end if
3768 plen = len_trim(prefix)
3769 slen = len_trim(suffix)
3770 ! Conservative estimate: max digits for any value in range + sign
3771 max_digits = 12 ! enough for any 32-bit integer
3772 buf_cap = num_values * (plen + max_digits + slen + 1)
3773 allocate(character(len=buf_cap) :: result_buf)
3774 buf_pos = 1
3775
3776 if (start_val <= end_val) then
3777 current_val = start_val
3778 do while (current_val <= end_val)
3779 write(num_str, '(I0)') current_val
3780 num_len = len_trim(num_str)
3781 if (buf_pos > 1) then
3782 result_buf(buf_pos:buf_pos) = ' '
3783 buf_pos = buf_pos + 1
3784 end if
3785 if (plen > 0) then
3786 result_buf(buf_pos:buf_pos+plen-1) = prefix(1:plen)
3787 buf_pos = buf_pos + plen
3788 end if
3789 result_buf(buf_pos:buf_pos+num_len-1) = num_str(1:num_len)
3790 buf_pos = buf_pos + num_len
3791 if (slen > 0) then
3792 result_buf(buf_pos:buf_pos+slen-1) = suffix(1:slen)
3793 buf_pos = buf_pos + slen
3794 end if
3795 current_val = current_val + step_val
3796 end do
3797 else
3798 current_val = start_val
3799 do while (current_val >= end_val)
3800 write(num_str, '(I0)') current_val
3801 num_len = len_trim(num_str)
3802 if (buf_pos > 1) then
3803 result_buf(buf_pos:buf_pos) = ' '
3804 buf_pos = buf_pos + 1
3805 end if
3806 if (plen > 0) then
3807 result_buf(buf_pos:buf_pos+plen-1) = prefix(1:plen)
3808 buf_pos = buf_pos + plen
3809 end if
3810 result_buf(buf_pos:buf_pos+num_len-1) = num_str(1:num_len)
3811 buf_pos = buf_pos + num_len
3812 if (slen > 0) then
3813 result_buf(buf_pos:buf_pos+slen-1) = suffix(1:slen)
3814 buf_pos = buf_pos + slen
3815 end if
3816 current_val = current_val - step_val
3817 end do
3818 end if
3819 expanded = result_buf(1:buf_pos-1)
3820 ! Recursively expand if result still contains braces
3821 if (index(expanded, '{') > 0) then
3822 expanded = recursive_expand_all_braces(expanded)
3823 end if
3824 return
3825 else if (is_alpha) then
3826 ! Alphabetic range expansion — O(n) with pre-allocated buffer
3827 plen = len_trim(prefix)
3828 slen = len_trim(suffix)
3829 if (start_char <= end_char) then
3830 num_values = (end_char - start_char) / step_val + 1
3831 else
3832 num_values = (start_char - end_char) / step_val + 1
3833 end if
3834 buf_cap = num_values * (plen + 1 + slen + 1)
3835 allocate(character(len=buf_cap) :: result_buf)
3836 buf_pos = 1
3837
3838 if (start_char <= end_char) then
3839 current_char = start_char
3840 do while (current_char <= end_char)
3841 if (buf_pos > 1) then
3842 result_buf(buf_pos:buf_pos) = ' '
3843 buf_pos = buf_pos + 1
3844 end if
3845 if (plen > 0) then
3846 result_buf(buf_pos:buf_pos+plen-1) = prefix(1:plen)
3847 buf_pos = buf_pos + plen
3848 end if
3849 result_buf(buf_pos:buf_pos) = char(current_char)
3850 buf_pos = buf_pos + 1
3851 if (slen > 0) then
3852 result_buf(buf_pos:buf_pos+slen-1) = suffix(1:slen)
3853 buf_pos = buf_pos + slen
3854 end if
3855 current_char = current_char + step_val
3856 end do
3857 else
3858 current_char = start_char
3859 do while (current_char >= end_char)
3860 if (buf_pos > 1) then
3861 result_buf(buf_pos:buf_pos) = ' '
3862 buf_pos = buf_pos + 1
3863 end if
3864 if (plen > 0) then
3865 result_buf(buf_pos:buf_pos+plen-1) = prefix(1:plen)
3866 buf_pos = buf_pos + plen
3867 end if
3868 result_buf(buf_pos:buf_pos) = char(current_char)
3869 buf_pos = buf_pos + 1
3870 if (slen > 0) then
3871 result_buf(buf_pos:buf_pos+slen-1) = suffix(1:slen)
3872 buf_pos = buf_pos + slen
3873 end if
3874 current_char = current_char - step_val
3875 end do
3876 end if
3877 expanded = result_buf(1:buf_pos-1)
3878 return
3879 end if
3880 else
3881 ! List expansion: {a,b,c} - respect nested braces when finding commas
3882 ! Only expand if there's at least one comma at depth 0
3883
3884 ! First pass: count commas to estimate buffer size
3885 found_comma = .false.
3886 num_values = 1
3887 depth = 0
3888 do i = 1, len_trim(brace_content)
3889 if (brace_content(i:i) == '{') then
3890 depth = depth + 1
3891 else if (brace_content(i:i) == '}') then
3892 depth = depth - 1
3893 else if (brace_content(i:i) == ',' .and. depth == 0) then
3894 found_comma = .true.
3895 num_values = num_values + 1
3896 end if
3897 end do
3898
3899 ! Only expand if we found at least one comma
3900 if (.not. found_comma) then
3901 ! No comma found at this level - check if inner content has braces to expand
3902 if (index(brace_content, '{') > 0) then
3903 item = recursive_expand_all_braces(brace_content)
3904 expanded = add_braces_to_words(item, prefix, suffix)
3905 return
3906 end if
3907 return
3908 end if
3909
3910 ! Pre-allocate buffer: each item up to full brace_content length + prefix + suffix + space
3911 plen = len_trim(prefix)
3912 slen = len_trim(suffix)
3913 buf_cap = num_values * (plen + len_trim(brace_content) + slen + 1)
3914 allocate(character(len=buf_cap) :: result_buf)
3915 buf_pos = 1
3916
3917 ! Second pass: extract items and write directly
3918 last_pos = 1
3919 depth = 0
3920 do i = 1, len_trim(brace_content)
3921 if (brace_content(i:i) == '{') then
3922 depth = depth + 1
3923 else if (brace_content(i:i) == '}') then
3924 depth = depth - 1
3925 else if (brace_content(i:i) == ',' .and. depth == 0) then
3926 item = brace_content(last_pos:i-1)
3927 num_len = len_trim(item)
3928 if (buf_pos > 1) then
3929 result_buf(buf_pos:buf_pos) = ' '
3930 buf_pos = buf_pos + 1
3931 end if
3932 if (plen > 0) then
3933 result_buf(buf_pos:buf_pos+plen-1) = prefix(1:plen)
3934 buf_pos = buf_pos + plen
3935 end if
3936 if (num_len > 0) then
3937 result_buf(buf_pos:buf_pos+num_len-1) = item(1:num_len)
3938 buf_pos = buf_pos + num_len
3939 end if
3940 if (slen > 0) then
3941 result_buf(buf_pos:buf_pos+slen-1) = suffix(1:slen)
3942 buf_pos = buf_pos + slen
3943 end if
3944 last_pos = i + 1
3945 end if
3946 end do
3947
3948 ! Don't forget last item
3949 item = brace_content(last_pos:)
3950 num_len = len_trim(item)
3951 if (buf_pos > 1) then
3952 result_buf(buf_pos:buf_pos) = ' '
3953 buf_pos = buf_pos + 1
3954 end if
3955 if (plen > 0) then
3956 result_buf(buf_pos:buf_pos+plen-1) = prefix(1:plen)
3957 buf_pos = buf_pos + plen
3958 end if
3959 if (num_len > 0) then
3960 result_buf(buf_pos:buf_pos+num_len-1) = item(1:num_len)
3961 buf_pos = buf_pos + num_len
3962 end if
3963 if (slen > 0) then
3964 result_buf(buf_pos:buf_pos+slen-1) = suffix(1:slen)
3965 buf_pos = buf_pos + slen
3966 end if
3967 expanded = result_buf(1:buf_pos-1)
3968 ! Recursively expand if result still contains braces
3969 if (index(expanded, '{') > 0) then
3970 expanded = recursive_expand_all_braces(expanded)
3971 end if
3972 return
3973 end if
3974
3975 end function expand_braces
3976
3977 ! --------------------------------------------------------------------------
3978 ! Expand braces and return results as separate words in an allocatable array.
3979 ! This avoids the MAX_TOKEN_LEN bottleneck of the space-separated string
3980 ! approach and matches bash/zsh behavior for arbitrarily large expansions.
3981 ! --------------------------------------------------------------------------
3982 subroutine expand_braces_to_words(word, words, word_count)
3983 character(len=*), intent(in) :: word
3984 character(len=MAX_TOKEN_LEN), allocatable, intent(out) :: words(:)
3985 integer, intent(out) :: word_count
3986
3987 character(len=:), allocatable :: expanded, wrd
3988 integer :: i, wstart, cap
3989
3990 ! Use existing expand_braces which returns space-separated result
3991 expanded = expand_braces(word)
3992
3993 ! Count words to allocate exact size
3994 word_count = 0
3995 if (len(expanded) == 0) then
3996 allocate(words(1))
3997 words(1) = word
3998 word_count = 1
3999 return
4000 end if
4001
4002 ! Count spaces to estimate word count
4003 cap = 1
4004 do i = 1, len(expanded)
4005 if (expanded(i:i) == ' ') cap = cap + 1
4006 end do
4007
4008 allocate(words(cap))
4009 word_count = 0
4010 wstart = 1
4011
4012 do i = 1, len(expanded) + 1
4013 if (i > len(expanded) .or. expanded(i:i) == ' ') then
4014 if (i > wstart) then
4015 word_count = word_count + 1
4016 words(word_count) = expanded(wstart:i - 1)
4017 end if
4018 wstart = i + 1
4019 end if
4020 end do
4021
4022 if (word_count == 0) then
4023 word_count = 1
4024 words(1) = word
4025 end if
4026 end subroutine expand_braces_to_words
4027
4028 ! Helper function to recursively expand all braces in space-separated results
4029 function recursive_expand_all_braces(input) result(output)
4030 character(len=*), intent(in) :: input
4031 character(len=:), allocatable :: output
4032 ! Use allocatable array to avoid static storage
4033 type(string_t), allocatable :: words(:)
4034 character(len=:), allocatable :: temp_result
4035 integer :: word_count, i, j, out_pos, capacity
4036 character(len=:), allocatable :: final_result
4037 integer :: final_result_capacity, final_result_len
4038 character(len=:), allocatable :: temp_piece
4039
4040 ! Allocate initial array
4041 allocate(words(20)) ! Start with reasonable size
4042 allocate(character(len=max(1, len_trim(input))) :: temp_result)
4043 capacity = 20
4044
4045 ! Allocate final_result buffer to avoid stack allocation
4046 final_result_capacity = max(512, len(input) * 2)
4047 allocate(character(len=final_result_capacity) :: final_result)
4048 final_result_len = 0
4049
4050 ! Split by spaces
4051 word_count = 0
4052 j = 1
4053 out_pos = 1
4054 do i = 1, len_trim(input)
4055 if (input(i:i) == ' ') then
4056 if (out_pos > 1) then
4057 word_count = word_count + 1
4058 ! Grow array if needed
4059 if (word_count > capacity) then
4060 call grow_expansion_array(words, capacity)
4061 end if
4062 words(word_count)%str = temp_result(:out_pos-1)
4063 out_pos = 1
4064 end if
4065 else
4066 temp_result(out_pos:out_pos) = input(i:i)
4067 out_pos = out_pos + 1
4068 end if
4069 end do
4070 ! Don't forget last word
4071 if (out_pos > 1) then
4072 word_count = word_count + 1
4073 ! Grow array if needed
4074 if (word_count > capacity) then
4075 call grow_expansion_array(words, capacity)
4076 end if
4077 words(word_count)%str = temp_result(:out_pos-1)
4078 end if
4079
4080 ! Recursively expand each word and recombine
4081 do i = 1, word_count
4082 if (index(words(i)%str, '{') > 0) then
4083 ! Still has braces - recurse
4084 temp_result = expand_braces(trim(words(i)%str))
4085 if (final_result_len > 0) then
4086 temp_piece = ' ' // trim(temp_result)
4087 else
4088 temp_piece = trim(temp_result)
4089 end if
4090 else
4091 ! No braces - use as-is
4092 if (final_result_len > 0) then
4093 temp_piece = ' ' // trim(words(i)%str)
4094 else
4095 temp_piece = trim(words(i)%str)
4096 end if
4097 end if
4098
4099 ! Grow buffer if needed
4100 if (final_result_len + len(temp_piece) > final_result_capacity) then
4101 call grow_string_buffer_exp(final_result, final_result_capacity, &
4102 max(final_result_capacity * 2, final_result_len + len(temp_piece) + 256), &
4103 final_result_len)
4104 end if
4105
4106 ! Append the piece
4107 final_result(final_result_len+1:final_result_len+len(temp_piece)) = temp_piece
4108 final_result_len = final_result_len + len(temp_piece)
4109 end do
4110
4111 output = final_result(:final_result_len)
4112
4113 ! Clean up allocatable arrays
4114 if (allocated(words)) deallocate(words)
4115 if (allocated(final_result)) deallocate(final_result)
4116 if (allocated(temp_piece)) deallocate(temp_piece)
4117 end function recursive_expand_all_braces
4118
4119 ! Helper function to add literal braces around each word in a space-separated list
4120 ! e.g., "a1 a2" with prefix="" and suffix="" becomes "{a1} {a2}"
4121 function add_braces_to_words(words_str, prefix, suffix) result(output)
4122 character(len=*), intent(in) :: words_str, prefix, suffix
4123 character(len=:), allocatable :: output
4124 character(len=:), allocatable :: result_buf, word_buf
4125 integer :: i, word_start, word_len
4126
4127 result_buf = ''
4128 word_start = 1
4129 i = 1
4130
4131 do while (i <= len_trim(words_str))
4132 if (words_str(i:i) == ' ') then
4133 ! End of word - add braces around it
4134 word_len = i - word_start
4135 if (word_len > 0) then
4136 word_buf = words_str(word_start:i-1)
4137 if (len_trim(result_buf) > 0) then
4138 result_buf = trim(result_buf) // ' ' // trim(prefix) // '{' // &
4139 trim(word_buf) // '}' // trim(suffix)
4140 else
4141 result_buf = trim(prefix) // '{' // trim(word_buf) // '}' // trim(suffix)
4142 end if
4143 end if
4144 word_start = i + 1
4145 end if
4146 i = i + 1
4147 end do
4148
4149 ! Handle last word
4150 if (word_start <= len_trim(words_str)) then
4151 word_buf = words_str(word_start:len_trim(words_str))
4152 if (len_trim(result_buf) > 0) then
4153 result_buf = trim(result_buf) // ' ' // trim(prefix) // '{' // &
4154 trim(word_buf) // '}' // trim(suffix)
4155 else
4156 result_buf = trim(prefix) // '{' // trim(word_buf) // '}' // trim(suffix)
4157 end if
4158 end if
4159
4160 output = trim(result_buf)
4161 end function add_braces_to_words
4162
4163 ! Helper subroutine to grow expansion array
4164 subroutine grow_expansion_array(array, current_size)
4165 type(string_t), allocatable, intent(inout) :: array(:)
4166 integer, intent(inout) :: current_size
4167 type(string_t), allocatable :: new_array(:)
4168 integer :: new_size, k
4169
4170 new_size = current_size * 2
4171 allocate(new_array(new_size))
4172
4173 ! Copy existing data
4174 do k = 1, current_size
4175 if (allocated(array(k)%str)) then
4176 new_array(k)%str = array(k)%str
4177 else
4178 new_array(k)%str = ''
4179 end if
4180 end do
4181
4182 ! Swap arrays
4183 call move_alloc(new_array, array)
4184 current_size = new_size
4185 end subroutine
4186
4187 ! Tilde expansion - expands ~ to home directory
4188 subroutine tilde_expansion(shell, input, output)
4189 type(shell_state_t), intent(inout) :: shell
4190 character(len=*), intent(in) :: input
4191 character(len=*), intent(out) :: output
4192 character(len=:), allocatable :: home_dir
4193 character(len=:), allocatable :: env_home
4194 integer :: tilde_pos
4195
4196 output = input
4197
4198 ! Find tilde at start of word
4199 tilde_pos = 1
4200 if (len_trim(input) == 0 .or. input(1:1) /= '~') return
4201
4202 ! Get home directory
4203 env_home = get_environment_var('HOME')
4204 if (allocated(env_home) .and. len(env_home) > 0) then
4205 home_dir = env_home
4206 else
4207 home_dir = '/home/' // trim(shell%username)
4208 end if
4209
4210 if (len_trim(input) == 1) then
4211 ! Just ~
4212 output = trim(home_dir)
4213 else if (input(2:2) == '/') then
4214 ! ~/path
4215 output = trim(home_dir) // input(2:)
4216 else if (input(2:2) == ' ' .or. input(2:2) == char(9)) then
4217 ! ~ followed by whitespace
4218 output = trim(home_dir) // input(2:)
4219 else
4220 ! ~username - not implemented, leave as-is
4221 ! TODO: Implement ~username expansion using getpwnam()
4222 return
4223 end if
4224 end subroutine
4225
4226 ! Check if input is a quoted literal with no expansions inside
4227 ! Returns true for "literal", 'literal', but false for "$var", "$(cmd)", etc.
4228 function is_quoted_literal(input) result(is_literal)
4229 character(len=*), intent(in) :: input
4230 logical :: is_literal
4231 integer :: len_input, i
4232 character(1) :: quote_char
4233
4234 is_literal = .false.
4235 len_input = len_trim(input)
4236
4237 ! Must be at least 2 chars for quotes
4238 if (len_input < 2) return
4239
4240 ! Check for matching outer quotes
4241 if (input(1:1) == '"' .and. input(len_input:len_input) == '"') then
4242 quote_char = '"'
4243 else if (input(1:1) == "'" .and. input(len_input:len_input) == "'") then
4244 quote_char = "'"
4245 ! Single quotes never have expansions, so it's always literal
4246 is_literal = .true.
4247 return
4248 else
4249 ! Not fully quoted
4250 return
4251 end if
4252
4253 ! For double quotes, check if there are expansion operators inside
4254 do i = 2, len_input - 1
4255 if (input(i:i) == '$' .or. input(i:i) == '`') then
4256 ! Has expansion - not a pure literal
4257 return
4258 end if
4259 end do
4260
4261 ! No expansion operators found
4262 is_literal = .true.
4263 end function
4264
4265 ! Complete word expansion including all POSIX expansions
4266 ! Order follows POSIX standard:
4267 ! 1. Brace expansion
4268 ! 2. Tilde expansion
4269 ! 3. Parameter and variable expansion
4270 ! 4. Quote removal
4271 ! 5. Field splitting
4272 subroutine expand_word(shell, input, expanded_words, word_count)
4273 type(shell_state_t), intent(inout) :: shell
4274 character(len=*), intent(in) :: input
4275 type(string_t), intent(out) :: expanded_words(:)
4276 integer, intent(out) :: word_count
4277
4278 character(len=:), allocatable :: temp_result, brace_expanded
4279 character(len=:), allocatable :: tilde_expanded, quote_removed
4280 character(len=:), allocatable :: temp_split_words(:)
4281 integer :: k
4282
4283 word_count = 1
4284
4285 ! Step 0: Brace expansion (happens FIRST, before all other expansions)
4286 brace_expanded = expand_braces(input)
4287
4288 ! Step 1: Tilde expansion
4289 ! Pre-allocate for intent(out) character(len=*) parameter
4290 allocate(character(len=len(brace_expanded) + 4096) :: tilde_expanded)
4291 call tilde_expansion(shell, brace_expanded, tilde_expanded)
4292
4293 ! Step 2: Parameter and variable expansion
4294 call enhanced_expand_variables(tilde_expanded, temp_result, shell)
4295
4296 ! Step 3: Quote removal
4297 quote_removed = remove_quotes(temp_result)
4298
4299 ! Step 4: Field splitting (if not quoted)
4300 ! POSIX: Field splitting only applies to results of parameter expansion,
4301 ! command substitution, and arithmetic expansion - NOT to literal quoted strings.
4302 ! Check if the original input was entirely quoted with no expansions inside.
4303 if (is_quoted_literal(input)) then
4304 ! Skip field splitting for quoted literals
4305 expanded_words(1)%str = quote_removed
4306 word_count = 1
4307 else
4308 ! Use temp buffer for word_split (expects character(len=*) array)
4309 allocate(character(len=max(1, len(quote_removed))) :: temp_split_words(size(expanded_words)))
4310 call word_split(shell, quote_removed, temp_split_words, word_count)
4311 do k = 1, word_count
4312 expanded_words(k)%str = trim(temp_split_words(k))
4313 end do
4314 deallocate(temp_split_words)
4315 end if
4316
4317 ! POSIX: If field splitting results in zero words (empty unquoted expansion),
4318 ! keep it as zero words - don't add back an empty field
4319 ! Note: This means unquoted empty variables disappear from the command line
4320 end subroutine
4321
4322 ! Helper to grow an allocatable string buffer
4323 subroutine grow_string_buffer_exp(buffer, old_capacity, new_capacity, content_len)
4324 character(len=:), allocatable, intent(inout) :: buffer
4325 integer, intent(inout) :: old_capacity
4326 integer, intent(in) :: new_capacity
4327 integer, intent(in) :: content_len ! Actual used length of buffer
4328 character(len=:), allocatable :: temp
4329
4330 ! Validate content_len
4331 if (content_len < 0 .or. content_len > old_capacity) then
4332 ! Invalid content length - this is a bug, but don't crash
4333 if (allocated(buffer)) deallocate(buffer)
4334 allocate(character(len=new_capacity) :: buffer)
4335 buffer = ''
4336 old_capacity = new_capacity
4337 return
4338 end if
4339
4340 ! Allocate temp buffer and copy only actual content
4341 allocate(character(len=new_capacity) :: temp)
4342 temp = '' ! Initialize entire buffer to prevent heap corruption
4343
4344 if (allocated(buffer) .and. content_len > 0) then
4345 ! Only copy the actual content (content_len bytes), not uninitialized data
4346 temp(1:content_len) = buffer(1:content_len)
4347 deallocate(buffer)
4348 end if
4349
4350 ! Allocate new larger buffer
4351 allocate(character(len=new_capacity) :: buffer)
4352 buffer = '' ! Initialize entire buffer
4353 if (content_len > 0) then
4354 buffer(1:content_len) = temp(1:content_len)
4355 end if
4356 old_capacity = new_capacity
4357
4358 deallocate(temp)
4359 end subroutine
4360
4361 ! Parse arithmetic number with octal and hex support
4362 ! Returns value and sets iostat (0 = success, non-zero = error)
4363 function parse_arithmetic_number(str, iostat) result(value)
4364 character(len=*), intent(in) :: str
4365 integer, intent(out) :: iostat
4366 integer(kind=8) :: value
4367 integer :: i, len_str, digit
4368 character(len=256) :: trimmed_str
4369
4370 value = 0
4371 iostat = 0
4372 trimmed_str = trim(adjustl(str))
4373 len_str = len_trim(trimmed_str)
4374
4375 if (len_str == 0) then
4376 iostat = 1
4377 return
4378 end if
4379
4380 ! Check for hexadecimal (0x or 0X)
4381 if (len_str >= 3 .and. trimmed_str(1:1) == '0' .and. &
4382 (trimmed_str(2:2) == 'x' .or. trimmed_str(2:2) == 'X')) then
4383 ! Parse hexadecimal
4384 do i = 3, len_str
4385 if (trimmed_str(i:i) >= '0' .and. trimmed_str(i:i) <= '9') then
4386 digit = ichar(trimmed_str(i:i)) - ichar('0')
4387 else if (trimmed_str(i:i) >= 'a' .and. trimmed_str(i:i) <= 'f') then
4388 digit = ichar(trimmed_str(i:i)) - ichar('a') + 10
4389 else if (trimmed_str(i:i) >= 'A' .and. trimmed_str(i:i) <= 'F') then
4390 digit = ichar(trimmed_str(i:i)) - ichar('A') + 10
4391 else
4392 iostat = 1
4393 return
4394 end if
4395 value = value * 16 + digit
4396 end do
4397 return
4398 end if
4399
4400 ! Check for octal (starts with 0 and has only 0-7 digits)
4401 if (len_str >= 2 .and. trimmed_str(1:1) == '0') then
4402 ! Verify all digits are 0-7 for octal
4403 do i = 2, len_str
4404 if (trimmed_str(i:i) < '0' .or. trimmed_str(i:i) > '7') then
4405 ! Not a valid octal, try decimal
4406 read(trimmed_str, *, iostat=iostat) value
4407 return
4408 end if
4409 end do
4410 ! Parse as octal
4411 do i = 1, len_str
4412 digit = ichar(trimmed_str(i:i)) - ichar('0')
4413 value = value * 8 + digit
4414 end do
4415 return
4416 end if
4417
4418 ! Default: parse as decimal
4419 ! First verify the string contains only valid decimal characters (digits and optional leading +/-)
4420 i = 1
4421 if (len_str > 0 .and. (trimmed_str(1:1) == '+' .or. trimmed_str(1:1) == '-')) then
4422 i = 2
4423 end if
4424 do while (i <= len_str)
4425 if (trimmed_str(i:i) < '0' .or. trimmed_str(i:i) > '9') then
4426 iostat = 1 ! Not a valid decimal number
4427 return
4428 end if
4429 i = i + 1
4430 end do
4431 read(trimmed_str, *, iostat=iostat) value
4432 end function
4433
4434 end module expansion