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