Fortran · 70697 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: variables
3 ! Purpose: Shell variable management and assignment
4 ! ==============================================================================
5 module variables
6 use shell_types
7 use system_interface
8 use iso_fortran_env, only: output_unit, error_unit
9 use io_helpers, only: write_stderr
10 #ifdef USE_C_STRINGS
11 use iso_c_binding, only: c_ptr, c_char, c_int, c_size_t
12 #endif
13 implicit none
14
15 #ifdef USE_C_STRINGS
16 interface
17 function c_vbuf_append_chars(handle, str, slen) result(rc) bind(C, name='fortsh_buffer_append_chars')
18 import :: c_ptr, c_int, c_char, c_size_t
19 type(c_ptr), value :: handle
20 character(kind=c_char), intent(in) :: str(*)
21 integer(c_size_t), value :: slen
22 integer(c_int) :: rc
23 end function
24 end interface
25 #endif
26
27 contains
28
29 ! Safe allocatable string assignment — works around flang-new ARM64 bug where
30 ! allocatable assignment corrupts values >16 bytes. Allocates to exact length
31 ! then copies character-by-character to avoid the substring temporary.
32 subroutine safe_assign_alloc_str(dest, src, src_len)
33 character(len=:), allocatable, intent(inout) :: dest
34 character(len=*), intent(in) :: src
35 integer, intent(in) :: src_len
36 integer :: k
37 if (allocated(dest)) deallocate(dest)
38 if (src_len <= 0) then
39 allocate(character(len=0) :: dest)
40 return
41 end if
42 allocate(character(len=src_len) :: dest)
43 do k = 1, src_len
44 dest(k:k) = src(k:k)
45 end do
46 end subroutine
47
48 subroutine set_shell_variable(shell, name, value, value_length)
49 use iso_fortran_env, only: error_unit
50 type(shell_state_t), intent(inout) :: shell
51 character(len=*), intent(in) :: name, value
52 integer, intent(in), optional :: value_length
53 integer :: i, empty_slot, iostat, actual_len, depth
54
55
56 empty_slot = -1
57
58 ! Calculate actual length (use provided length or len_trim as fallback)
59 if (present(value_length)) then
60 actual_len = value_length
61 else
62 actual_len = len_trim(value)
63 end if
64
65 ! First check local variables - if variable exists in current function scope, update it there
66 if (shell%function_depth > 0) then
67 depth = shell%function_depth
68 if (depth <= size(shell%local_var_counts)) then
69 do i = 1, shell%local_var_counts(depth)
70 if (trim(shell%local_vars(depth, i)%name) == trim(name)) then
71 ! Found existing local variable - update it
72 call safe_assign_alloc_str(shell%local_vars(depth, i)%value, value, actual_len)
73 shell%local_vars(depth, i)%value_len = actual_len
74 return
75 end if
76 end do
77 end if
78 end if
79
80 ! Handle special built-in variables
81 select case (trim(name))
82 case ('PS1')
83 shell%ps1 = value
84 ! For prompts, always use len_trim to get actual content length
85 shell%ps1_len = len_trim(value)
86 return
87 case ('PS2')
88 shell%ps2 = value
89 if (present(value_length)) then
90 shell%ps2_len = value_length
91 else
92 shell%ps2_len = len(value)
93 end if
94 return
95 case ('PS3')
96 shell%ps3 = value
97 if (present(value_length)) then
98 shell%ps3_len = value_length
99 else
100 shell%ps3_len = len(value)
101 end if
102 return
103 case ('PS4')
104 shell%ps4 = value
105 if (present(value_length)) then
106 shell%ps4_len = value_length
107 else
108 shell%ps4_len = len(value)
109 end if
110 return
111 case ('IFS')
112 shell%ifs = value(1:actual_len)
113 shell%ifs_len = actual_len
114 ! Don't return - continue to add IFS to variables array too
115 ! This allows checking if IFS was explicitly set vs using default
116 case ('PWD')
117 ! Update shell%cwd when PWD is set
118 shell%cwd = value(1:min(actual_len, len(shell%cwd)))
119 ! Also update environment for child processes
120 if (.not. set_environment_var('PWD', trim(shell%cwd))) then
121 ! Silently ignore errors
122 end if
123 return
124 case ('OLDPWD')
125 ! Update shell%oldpwd when OLDPWD is set
126 shell%oldpwd = value(1:min(actual_len, len(shell%oldpwd)))
127 ! Also update environment for child processes
128 if (.not. set_environment_var('OLDPWD', trim(shell%oldpwd))) then
129 ! Silently ignore errors
130 end if
131 return
132 case ('PATH')
133 ! PATH must ALWAYS update environment so child processes use new PATH
134 if (.not. set_environment_var('PATH', value(1:actual_len))) then
135 ! Silently ignore errors
136 end if
137 ! Clear hash table when PATH changes (bash behavior)
138 shell%num_hashed_commands = 0
139 ! Don't return - continue to store in variables array too
140 case ('HISTFILE')
141 shell%histfile = value
142 return
143 case ('HISTSIZE')
144 read(value, *, iostat=iostat) shell%histsize
145 if (iostat /= 0) shell%histsize = 1000
146 return
147 case ('HISTFILESIZE')
148 read(value, *, iostat=iostat) shell%histfilesize
149 if (iostat /= 0) shell%histfilesize = 2000
150 return
151 case ('HISTCONTROL')
152 shell%histcontrol = value
153 ! Note: histcontrol is also updated in fortsh.f90 via set_histcontrol()
154 return
155 end select
156
157 ! Check if variable already exists
158 do i = 1, shell%num_variables
159 if (trim(shell%variables(i)%name) == trim(name)) then
160 ! Check if variable is readonly
161 if (shell%variables(i)%readonly) then
162 block
163 character(len=32) :: line_str
164 write(line_str, '(i0)') shell%current_line_number
165 call write_stderr('fortsh: line ' // trim(line_str) // ': ' // &
166 trim(name) // ': readonly variable')
167 end block
168 shell%last_exit_status = 1 ! POSIX: readonly assignment failure returns 1
169 ! POSIX: In non-interactive shells, stop execution after readonly violation
170 if (.not. shell%is_interactive) then
171 shell%running = .false.
172 end if
173 return
174 end if
175 call safe_assign_alloc_str(shell%variables(i)%value, value, actual_len)
176 shell%variables(i)%value_len = actual_len
177 ! If exported, update environment
178 if (shell%variables(i)%exported) then
179 if (.not. set_environment_var(trim(name), value(1:actual_len))) then
180 write(error_unit, '(a)') 'warning: failed to update environment variable'
181 end if
182 end if
183 return
184 end if
185 end do
186
187 ! Find empty slot
188 do i = 1, size(shell%variables)
189 ! Check for empty name (null character or spaces)
190 if (shell%variables(i)%name(1:1) == char(0) .or. trim(shell%variables(i)%name) == '') then
191 empty_slot = i
192 exit
193 end if
194 end do
195
196 ! Add new variable
197 if (empty_slot > 0) then
198 shell%variables(empty_slot)%name = name
199 call safe_assign_alloc_str(shell%variables(empty_slot)%value, value, actual_len)
200 shell%variables(empty_slot)%value_len = actual_len
201 shell%num_variables = shell%num_variables + 1
202 end if
203 end subroutine
204
205 function get_shell_variable(shell, name) result(value)
206 type(shell_state_t), intent(in) :: shell
207 character(len=*), intent(in) :: name
208 character(len=:), allocatable :: value
209 integer :: i, depth
210 character(len=20) :: fmt_buf
211
212 value = ''
213
214 ! First check local variables (innermost scope first)
215 if (shell%function_depth > 0) then
216 do depth = shell%function_depth, 1, -1
217 if (depth <= size(shell%local_var_counts)) then
218 do i = 1, shell%local_var_counts(depth)
219 if (trim(shell%local_vars(depth, i)%name) == trim(name)) then
220 ! value_len=-1 means explicitly unset local (shadows global)
221 if (shell%local_vars(depth, i)%value_len == -1) then
222 value = ''
223 return
224 end if
225 value = shell%local_vars(depth, i)%value
226 return
227 end if
228 end do
229 end if
230 end do
231 end if
232
233 ! Handle special variables
234 select case (trim(name))
235 case ('$')
236 write(fmt_buf, '(i0)') shell%shell_pid
237 value = trim(adjustl(fmt_buf))
238 return
239 case ('!')
240 write(fmt_buf, '(i0)') shell%last_bg_pid
241 value = trim(adjustl(fmt_buf))
242 return
243 case ('?')
244 write(fmt_buf, '(i0)') shell%last_exit_status
245 value = trim(adjustl(fmt_buf))
246 return
247 case ('0')
248 value = trim(shell%shell_name)
249 return
250 case ('_')
251 ! Last argument of previous command
252 value = trim(shell%last_arg)
253 return
254 case ('-')
255 ! Current shell options as flags
256 value = get_shell_option_flags(shell)
257 return
258 case ('PPID')
259 write(fmt_buf, '(i0)') int(shell%parent_pid)
260 value = trim(adjustl(fmt_buf))
261 return
262 case ('UID')
263 write(fmt_buf, '(i0)') shell%uid
264 value = trim(adjustl(fmt_buf))
265 return
266 case ('EUID')
267 write(fmt_buf, '(i0)') shell%euid
268 value = trim(adjustl(fmt_buf))
269 return
270 case ('PWD')
271 value = trim(shell%cwd)
272 return
273 case ('OLDPWD')
274 value = trim(shell%oldpwd)
275 return
276 case ('RANDOM')
277 write(fmt_buf, '(i0)') get_random_int()
278 value = trim(adjustl(fmt_buf))
279 return
280 case ('SECONDS')
281 write(fmt_buf, '(i0)') get_elapsed_seconds(shell)
282 value = trim(adjustl(fmt_buf))
283 return
284 case ('LINENO')
285 write(fmt_buf, '(i0)') shell%current_line_number
286 value = trim(adjustl(fmt_buf))
287 return
288 case ('#')
289 write(fmt_buf, '(I0)') shell%num_positional
290 value = trim(adjustl(fmt_buf))
291 return
292 case ('*')
293 block
294 character(len=4096) :: params_buf
295 call get_all_positional_params(shell, params_buf, .true.)
296 value = trim(params_buf)
297 end block
298 return
299 case ('@')
300 block
301 character(len=4096) :: params_buf
302 call get_all_positional_params(shell, params_buf, .false.)
303 value = trim(params_buf)
304 end block
305 return
306 case ('IFS')
307 ! Internal field separator - use ifs_len to preserve whitespace
308 if (shell%ifs_len > 0) then
309 value = shell%ifs(1:shell%ifs_len)
310 else
311 value = ''
312 end if
313 return
314 case ('PS1')
315 value = shell%ps1
316 return
317 case ('PS2')
318 value = shell%ps2
319 return
320 case ('PS3')
321 value = shell%ps3
322 return
323 case ('PS4')
324 value = shell%ps4
325 return
326 case ('HISTFILE')
327 value = trim(shell%histfile)
328 return
329 case ('HISTSIZE')
330 write(fmt_buf, '(i0)') shell%histsize
331 value = trim(adjustl(fmt_buf))
332 return
333 case ('HISTFILESIZE')
334 write(fmt_buf, '(i0)') shell%histfilesize
335 value = trim(adjustl(fmt_buf))
336 return
337 case ('HISTCONTROL')
338 value = trim(shell%histcontrol)
339 return
340 end select
341
342 ! Handle numeric positional parameters ($1, $2, ..., $n)
343 if (is_numeric(trim(name))) then
344 i = string_to_int(trim(name))
345 if (i >= 1 .and. i <= shell%num_positional) then
346 value = trim(shell%positional_params(i)%str)
347 return
348 else
349 value = ''
350 return
351 end if
352 end if
353
354 ! Handle regular shell variables
355 do i = 1, shell%num_variables
356 if (trim(shell%variables(i)%name) == trim(name)) then
357 ! Use value_len to preserve trailing whitespace
358 if (shell%variables(i)%value_len > 0) then
359 value = shell%variables(i)%value(1:shell%variables(i)%value_len)
360 else
361 value = shell%variables(i)%value
362 end if
363 return
364 end if
365 end do
366
367 ! Handle environment variables if not found in shell variables
368 value = get_environment_var(trim(name))
369 end function
370
371 #ifdef USE_C_STRINGS
372 ! Copy variable value directly into a C buffer — no allocatable intermediaries.
373 ! This avoids the flang-new heap corruption that occurs when large (>2KB)
374 ! allocatable strings are returned from get_shell_variable.
375 subroutine get_shell_variable_to_cbuf(shell, name, cbuf)
376 type(shell_state_t), intent(in) :: shell
377 character(len=*), intent(in) :: name
378 type(c_ptr), intent(in) :: cbuf
379 integer :: i, depth, vlen, rc
380 character(len=20) :: fmt_buf
381
382 ! Check local variables first
383 if (shell%function_depth > 0) then
384 do depth = shell%function_depth, 1, -1
385 if (depth <= size(shell%local_var_counts)) then
386 do i = 1, shell%local_var_counts(depth)
387 if (trim(shell%local_vars(depth, i)%name) == trim(name)) then
388 if (shell%local_vars(depth, i)%value_len == -1) return
389 vlen = len_trim(shell%local_vars(depth, i)%value)
390 if (vlen > 0) rc = c_vbuf_append_chars(cbuf, &
391 shell%local_vars(depth, i)%value, int(vlen, c_size_t))
392 return
393 end if
394 end do
395 end if
396 end do
397 end if
398
399 ! Handle special variables (small strings, safe to use fmt_buf)
400 select case (trim(name))
401 case ('$')
402 write(fmt_buf, '(i0)') shell%shell_pid
403 vlen = len_trim(fmt_buf)
404 rc = c_vbuf_append_chars(cbuf, fmt_buf, int(vlen, c_size_t))
405 return
406 case ('!')
407 write(fmt_buf, '(i0)') shell%last_bg_pid
408 vlen = len_trim(fmt_buf)
409 rc = c_vbuf_append_chars(cbuf, fmt_buf, int(vlen, c_size_t))
410 return
411 case ('?')
412 write(fmt_buf, '(i0)') shell%last_exit_status
413 vlen = len_trim(fmt_buf)
414 rc = c_vbuf_append_chars(cbuf, fmt_buf, int(vlen, c_size_t))
415 return
416 case ('0')
417 vlen = len_trim(shell%shell_name)
418 if (vlen > 0) rc = c_vbuf_append_chars(cbuf, shell%shell_name, int(vlen, c_size_t))
419 return
420 case ('_')
421 vlen = len_trim(shell%last_arg)
422 if (vlen > 0) rc = c_vbuf_append_chars(cbuf, shell%last_arg, int(vlen, c_size_t))
423 return
424 end select
425
426 ! Handle regular shell variables — copy directly from storage, no allocatable
427 do i = 1, shell%num_variables
428 if (trim(shell%variables(i)%name) == trim(name)) then
429 if (shell%variables(i)%value_len > 0) then
430 vlen = shell%variables(i)%value_len
431 else
432 vlen = len_trim(shell%variables(i)%value)
433 end if
434 if (vlen > 0) rc = c_vbuf_append_chars(cbuf, &
435 shell%variables(i)%value, int(vlen, c_size_t))
436 return
437 end if
438 end do
439
440 ! Fall back to environment (small strings, safe)
441 ! Use get_shell_variable for the env fallback since env vars are small
442 block
443 character(len=:), allocatable :: env_val
444 env_val = get_environment_var(trim(name))
445 vlen = len_trim(env_val)
446 if (vlen > 0) rc = c_vbuf_append_chars(cbuf, env_val, int(vlen, c_size_t))
447 end block
448 end subroutine
449 #endif
450
451 function is_shell_variable_set(shell, name) result(is_set)
452 type(shell_state_t), intent(in) :: shell
453 character(len=*), intent(in) :: name
454 logical :: is_set
455 integer :: i, depth
456
457 is_set = .false.
458
459 ! First check local variables (innermost scope first)
460 if (shell%function_depth > 0) then
461 do depth = shell%function_depth, 1, -1
462 if (depth <= size(shell%local_var_counts)) then
463 do i = 1, shell%local_var_counts(depth)
464 if (trim(shell%local_vars(depth, i)%name) == trim(name)) then
465 ! value_len=-1 means explicitly unset local (shadows global)
466 if (shell%local_vars(depth, i)%value_len == -1) then
467 is_set = .false.
468 else
469 is_set = .true.
470 end if
471 return
472 end if
473 end do
474 end if
475 end do
476 end if
477
478 ! Check special variables (most are always set)
479 select case (trim(name))
480 case ('$', '!', '?', '0', '_', '-', 'PPID', 'UID', 'EUID', &
481 'PWD', 'OLDPWD', 'RANDOM', 'SECONDS', 'LINENO', '#', '*', '@', &
482 'IFS', 'PS1', 'PS2', 'PS3', 'PS4', 'HISTFILE', 'HISTSIZE', &
483 'HISTFILESIZE', 'HISTCONTROL')
484 is_set = .true.
485 return
486 end select
487
488 ! Handle numeric positional parameters ($1, $2, ..., $n)
489 if (is_numeric(trim(name))) then
490 i = string_to_int(trim(name))
491 if (i >= 1 .and. i <= shell%num_positional) then
492 is_set = .true.
493 return
494 end if
495 end if
496
497 ! Check regular shell variables
498 do i = 1, shell%num_variables
499 if (trim(shell%variables(i)%name) == trim(name)) then
500 is_set = .true.
501 return
502 end if
503 end do
504
505 ! Check environment variables
506 if (len_trim(get_environment_var(trim(name))) > 0) then
507 is_set = .true.
508 end if
509 end function
510
511 ! Get the actual length of a shell variable (preserving whitespace)
512 function get_shell_variable_length(shell, name) result(var_len)
513 type(shell_state_t), intent(in) :: shell
514 character(len=*), intent(in) :: name
515 integer :: var_len
516 integer :: i, depth
517 character(len=20) :: temp_value
518
519 var_len = 0
520
521 ! First check local variables (innermost scope first)
522 if (shell%function_depth > 0) then
523 do depth = shell%function_depth, 1, -1
524 if (depth <= size(shell%local_var_counts)) then
525 do i = 1, shell%local_var_counts(depth)
526 if (trim(shell%local_vars(depth, i)%name) == trim(name)) then
527 ! value_len=-1 means explicitly unset local
528 if (shell%local_vars(depth, i)%value_len == -1) then
529 var_len = 0
530 else
531 var_len = len_trim(shell%local_vars(depth, i)%value)
532 end if
533 return
534 end if
535 end do
536 end if
537 end do
538 end if
539
540 ! Handle special variables
541 select case (trim(name))
542 case ('IFS')
543 ! Use ifs_len to preserve whitespace (even if it's all spaces)
544 var_len = shell%ifs_len
545 return
546 case ('PS1')
547 var_len = shell%ps1_len
548 return
549 case ('PS2')
550 var_len = shell%ps2_len
551 return
552 case ('PS3')
553 var_len = shell%ps3_len
554 return
555 case ('PS4')
556 var_len = shell%ps4_len
557 return
558 case ('PWD')
559 var_len = len_trim(shell%cwd)
560 return
561 case ('OLDPWD')
562 var_len = len_trim(shell%oldpwd)
563 return
564 case ('?')
565 write(temp_value, '(i15)') shell%last_exit_status
566 var_len = len_trim(adjustl(temp_value))
567 return
568 case ('#')
569 write(temp_value, '(i15)') shell%num_positional
570 var_len = len_trim(adjustl(temp_value))
571 return
572 case ('0')
573 var_len = len_trim(shell%shell_name)
574 return
575 case ('$')
576 write(temp_value, '(i0)') shell%shell_pid
577 var_len = len_trim(temp_value)
578 return
579 case ('PPID')
580 write(temp_value, '(i15)') shell%parent_pid
581 var_len = len_trim(adjustl(temp_value))
582 return
583 case ('UID')
584 write(temp_value, '(i15)') shell%uid
585 var_len = len_trim(adjustl(temp_value))
586 return
587 case ('EUID')
588 write(temp_value, '(i15)') shell%euid
589 var_len = len_trim(adjustl(temp_value))
590 return
591 case ('SECONDS')
592 var_len = 10 ! Max digits for seconds
593 return
594 case ('RANDOM')
595 var_len = 5 ! Max digits for RANDOM (0-32767)
596 return
597 case ('LINENO')
598 write(temp_value, '(i15)') shell%current_line_number
599 var_len = len_trim(adjustl(temp_value))
600 return
601 case ('HISTFILE')
602 var_len = len_trim(shell%histfile)
603 return
604 case ('HISTSIZE')
605 write(temp_value, '(i15)') shell%histsize
606 var_len = len_trim(adjustl(temp_value))
607 return
608 case ('HISTFILESIZE')
609 write(temp_value, '(i15)') shell%histfilesize
610 var_len = len_trim(adjustl(temp_value))
611 return
612 case ('HISTCONTROL')
613 var_len = len_trim(shell%histcontrol)
614 return
615 ! Note: No case default here - fall through to regular variable handling
616 end select
617
618 ! Handle regular shell variables
619 do i = 1, shell%num_variables
620 if (trim(shell%variables(i)%name) == trim(name)) then
621 ! Use value_len to preserve trailing whitespace
622 if (shell%variables(i)%value_len > 0) then
623 var_len = shell%variables(i)%value_len
624 else
625 var_len = len_trim(shell%variables(i)%value)
626 end if
627 return
628 end if
629 end do
630
631 ! Check environment variables
632 block
633 character(len=:), allocatable :: env_val
634 env_val = get_environment_var(trim(name))
635 if (allocated(env_val) .and. len(env_val) > 0) then
636 var_len = len(env_val)
637 return
638 end if
639 end block
640
641 ! Not found - return 0
642 var_len = 0
643 end function
644
645 function is_assignment(input_line) result(is_assign)
646 character(len=*), intent(in) :: input_line
647 logical :: is_assign
648 integer :: eq_pos
649
650 eq_pos = index(input_line, '=')
651 is_assign = (eq_pos > 1 .and. eq_pos < len_trim(input_line))
652 end function
653
654 subroutine handle_assignment(shell, input_line)
655 type(shell_state_t), intent(inout) :: shell
656 character(len=*), intent(in) :: input_line
657 integer :: eq_pos, bracket_pos, bracket_end, array_index, read_status
658 integer :: actual_value_len, i
659 character(len=256) :: var_name, index_str
660 character(len=:), allocatable :: var_value
661 character(len=:), allocatable :: expanded_value
662 character(len=1) :: quote_char_temp
663
664 eq_pos = index(input_line, '=')
665 if (eq_pos > 1) then
666 var_name = input_line(:eq_pos-1)
667 var_value = input_line(eq_pos+1:)
668
669 ! Calculate actual content length BEFORE stripping quotes (to preserve trailing spaces)
670 actual_value_len = len_trim(var_value)
671 if (actual_value_len >= 2) then
672 if (var_value(1:1) == "'" .or. var_value(1:1) == '"') then
673 ! Find closing quote position by searching backwards
674 quote_char_temp = var_value(1:1)
675 do i = actual_value_len, 2, -1
676 if (var_value(i:i) == quote_char_temp) then
677 ! Content length is closing_quote_pos - 2
678 actual_value_len = i - 2
679 exit
680 end if
681 end do
682 else
683 ! No quotes, use len_trim
684 actual_value_len = len_trim(var_value)
685 end if
686 else
687 actual_value_len = len_trim(var_value)
688 end if
689
690 ! Strip surrounding quotes from value
691 call strip_quotes(var_value)
692
693 ! Check for indexed/associative array assignment: arr[index]=value or map[key]=value
694 bracket_pos = index(var_name, '[')
695 if (bracket_pos > 0) then
696 ! arr[index]=value or map[key]=value
697 bracket_end = index(var_name(bracket_pos:), ']')
698 if (bracket_end > 0) then
699 bracket_end = bracket_pos + bracket_end - 1
700 index_str = var_name(bracket_pos+1:bracket_end-1)
701 var_name = var_name(:bracket_pos-1)
702
703 ! Strip quotes and lexer sentinel chars from array key
704 call strip_quotes(index_str)
705 block
706 character(len=100) :: clean_key
707 integer :: ci, co
708 co = 0
709 clean_key = ''
710 do ci = 1, len_trim(index_str)
711 if (ichar(index_str(ci:ci)) > 3) then
712 co = co + 1
713 clean_key(co:co) = index_str(ci:ci)
714 end if
715 end do
716 index_str = clean_key
717 end block
718
719 ! Check if this is an associative array
720 if (is_associative_array(shell, trim(var_name))) then
721 ! Associative array: use key as-is
722 call set_assoc_array_value(shell, trim(var_name), trim(index_str), trim(var_value))
723 shell%last_exit_status = 0
724 else
725 ! Try to parse as numeric index for indexed array
726 read(index_str, *, iostat=read_status) array_index
727 if (read_status == 0) then
728 ! Valid numeric index
729 array_index = array_index + 1 ! Convert to 1-indexed
730 call set_array_element(shell, trim(var_name), array_index, trim(var_value))
731 shell%last_exit_status = 0
732 else
733 ! Non-numeric index for non-associative array - error or treat as associative
734 call set_assoc_array_value(shell, trim(var_name), trim(index_str), trim(var_value))
735 shell%last_exit_status = 0
736 end if
737 end if
738 else
739 shell%last_exit_status = 1
740 end if
741 return
742 end if
743
744 ! Check for array literal assignment: var=(value1 value2 value3)
745 if (len_trim(var_value) > 2 .and. var_value(1:1) == '(' .and. &
746 var_value(len_trim(var_value):len_trim(var_value)) == ')') then
747 call handle_array_assignment(shell, trim(var_name), var_value)
748 else
749 ! Simple variable expansion during assignment
750 ! Check if value needs expansion (contains $ or ~)
751 if (index(var_value, '$') > 0 .or. index(var_value, '~') > 0) then
752 ! Needs expansion
753 call simple_expand_variables(var_value, expanded_value, shell)
754 ! For expanded values, use the allocated length
755 call set_shell_variable(shell, trim(var_name), expanded_value, len(expanded_value))
756 else
757 ! No expansion needed, preserve trailing spaces
758 call set_shell_variable(shell, trim(var_name), var_value, actual_value_len)
759 end if
760 end if
761 ! Set exit status to 0 for successful assignments
762 ! Don't overwrite error codes like 1 (readonly violation)
763 if (shell%last_exit_status /= 1) then
764 shell%last_exit_status = 0
765 end if
766 else
767 shell%last_exit_status = 1
768 end if
769 end subroutine
770
771 subroutine handle_array_assignment(shell, var_name, array_expr)
772 type(shell_state_t), intent(inout) :: shell
773 character(len=*), intent(in) :: var_name, array_expr
774 ! Use allocatable array to avoid static storage
775 type(string_t), allocatable :: values(:)
776 integer :: count, start_pos, pos, capacity
777 character(len=:), allocatable :: content
778 logical :: in_quotes
779
780 ! Remove parentheses
781 content = array_expr(2:len_trim(array_expr)-1)
782
783 ! Allocate initial array
784 allocate(values(20)) ! Start with reasonable size
785 capacity = 20
786 count = 0
787 pos = 1
788 start_pos = 1
789 in_quotes = .false.
790
791 ! Parse space-separated values, respecting quotes
792 do while (pos <= len_trim(content))
793 if (content(pos:pos) == '"' .or. content(pos:pos) == "'") then
794 in_quotes = .not. in_quotes
795 else if ((content(pos:pos) == ' ' .or. content(pos:pos) == char(10) .or. &
796 content(pos:pos) == char(9)) .and. .not. in_quotes) then
797 if (pos > start_pos) then
798 count = count + 1
799 ! Grow array if needed
800 if (count > capacity) then
801 call grow_string_array(values, capacity)
802 end if
803 values(count)%str = content(start_pos:pos-1)
804 ! Remove quotes if present
805 if (len_trim(values(count)%str) >= 2) then
806 if ((values(count)%str(1:1) == '"' .and. &
807 values(count)%str(len_trim(values(count)%str):len_trim(values(count)%str)) == '"') .or. &
808 (values(count)%str(1:1) == "'" .and. &
809 values(count)%str(len_trim(values(count)%str):len_trim(values(count)%str)) == "'")) then
810 values(count)%str = values(count)%str(2:len_trim(values(count)%str)-1)
811 end if
812 end if
813 end if
814 start_pos = pos + 1
815 end if
816 pos = pos + 1
817 end do
818
819 ! Handle last value
820 if (start_pos <= len_trim(content)) then
821 count = count + 1
822 ! Grow array if needed
823 if (count > capacity) then
824 call grow_string_array(values, capacity)
825 end if
826 values(count)%str = content(start_pos:)
827 ! Remove quotes if present
828 if (len_trim(values(count)%str) >= 2) then
829 if ((values(count)%str(1:1) == '"' .and. &
830 values(count)%str(len_trim(values(count)%str):len_trim(values(count)%str)) == '"') .or. &
831 (values(count)%str(1:1) == "'" .and. &
832 values(count)%str(len_trim(values(count)%str):len_trim(values(count)%str)) == "'")) then
833 values(count)%str = values(count)%str(2:len_trim(values(count)%str)-1)
834 end if
835 end if
836 end if
837
838 if (count > 0) then
839 call set_array_variable_string_t(shell, var_name, values(1:count), count)
840 end if
841
842 ! Clean up allocatable array
843 if (allocated(values)) deallocate(values)
844 end subroutine
845
846 ! Helper subroutine to grow string array
847 subroutine grow_string_array(array, current_size)
848 type(string_t), allocatable, intent(inout) :: array(:)
849 integer, intent(inout) :: current_size
850 type(string_t), allocatable :: new_array(:)
851 integer :: new_size, k
852
853 new_size = current_size * 2
854 allocate(new_array(new_size))
855
856 ! Copy existing data
857 do k = 1, current_size
858 if (allocated(array(k)%str)) then
859 new_array(k)%str = array(k)%str
860 else
861 new_array(k)%str = ''
862 end if
863 end do
864
865 ! Swap arrays
866 call move_alloc(new_array, array)
867 current_size = new_size
868 end subroutine
869
870 subroutine simple_expand_variables(input, expanded, shell)
871 character(len=*), intent(in) :: input
872 character(len=:), allocatable, intent(out) :: expanded
873 type(shell_state_t), intent(inout) :: shell
874
875 character(len=2048) :: result
876 integer :: i, j, var_start, brace_end
877 character(len=256) :: var_name
878 character(len=:), allocatable :: expansion_result, var_value
879 character(len=:), allocatable :: env_value
880
881 result = ''
882 i = 1
883 j = 1
884
885 do while (i <= len_trim(input))
886 if (input(i:i) == '$' .and. i < len_trim(input)) then
887 i = i + 1
888
889 ! Handle ${parameter} expansions
890 if (i <= len_trim(input) .and. input(i:i) == '{') then
891 i = i + 1
892 brace_end = index(input(i:), '}')
893 if (brace_end > 0) then
894 brace_end = brace_end + i - 1
895 call expand_parameter(input(i:brace_end-1), expansion_result, shell)
896 if (len_trim(expansion_result) > 0) then
897 result(j:j+len_trim(expansion_result)-1) = trim(expansion_result)
898 j = j + len_trim(expansion_result)
899 end if
900 i = brace_end + 1
901 else
902 ! Malformed ${, treat as literal
903 result(j:j) = '$'
904 result(j+1:j+1) = '{'
905 j = j + 2
906 end if
907 else
908 ! Handle simple $variable expansions
909 var_start = i
910
911 ! Check for special single-character variables first
912 if (i <= len_trim(input)) then
913 select case (input(i:i))
914 case ('!', '?', '$', '#', '*', '@', '-', '_', '0')
915 ! Single-character special variable
916 var_name = input(i:i)
917 i = i + 1
918 case default
919 ! Extract alphanumeric variable name
920 do while (i <= len_trim(input))
921 if (.not. (is_alnum(input(i:i)) .or. input(i:i) == '_')) exit
922 i = i + 1
923 end do
924 var_name = input(var_start:i-1)
925 end select
926 else
927 var_name = ''
928 end if
929
930 ! Check shell variables first
931 if (len_trim(var_name) > 0) then
932 var_value = get_shell_variable(shell, trim(var_name))
933 block
934 integer :: var_len
935 var_len = get_shell_variable_length(shell, trim(var_name))
936 if (var_len > 0) then
937 ! Use actual length to preserve trailing whitespace
938 result(j:j+var_len-1) = var_value(1:var_len)
939 j = j + var_len
940 else if (len_trim(var_value) > 0) then
941 ! Fallback for compatibility
942 result(j:j+len_trim(var_value)-1) = trim(var_value)
943 j = j + len_trim(var_value)
944 else
945 ! Fall back to environment variables (not for special vars)
946 if (.not. any(var_name == ['!', '?', '$', '#', '*', '@', '-', '_', '0'])) then
947 env_value = get_environment_var(trim(var_name))
948 if (allocated(env_value) .and. len(env_value) > 0) then
949 result(j:j+len(env_value)-1) = env_value
950 j = j + len(env_value)
951 end if
952 end if
953 end if
954 end block
955 end if
956 end if
957 else
958 result(j:j) = input(i:i)
959 i = i + 1
960 j = j + 1
961 end if
962 end do
963
964 ! Don't use trim() - preserve trailing whitespace
965 if (j > 1) then
966 expanded = result(1:j-1)
967 else
968 expanded = ''
969 end if
970
971 contains
972 function is_alnum(ch) result(res)
973 character, intent(in) :: ch
974 logical :: res
975 res = (ch >= 'a' .and. ch <= 'z') .or. &
976 (ch >= 'A' .and. ch <= 'Z') .or. &
977 (ch >= '0' .and. ch <= '9')
978 end function
979 end subroutine
980
981 subroutine add_function(shell, name, body_lines, body_count)
982 type(shell_state_t), intent(inout) :: shell
983 character(len=*), intent(in) :: name
984 character(len=*), intent(in) :: body_lines(:)
985 integer, intent(in) :: body_count
986 integer :: i, j
987
988 ! Find empty slot or replace existing function
989 do i = 1, size(shell%functions)
990 if (trim(shell%functions(i)%name) == trim(name) .or. len_trim(shell%functions(i)%name) == 0) then
991 shell%functions(i)%name = name
992 shell%functions(i)%body_lines = body_count
993
994 if (allocated(shell%functions(i)%body)) deallocate(shell%functions(i)%body)
995 allocate(shell%functions(i)%body(body_count))
996
997 do j = 1, body_count
998 shell%functions(i)%body(j)%str = trim(body_lines(j))
999 end do
1000
1001 ! Update function count to include this function
1002 shell%num_functions = max(shell%num_functions, i)
1003 return
1004 end if
1005 end do
1006 end subroutine
1007
1008 function is_function(shell, name) result(found)
1009 type(shell_state_t), intent(in) :: shell
1010 character(len=*), intent(in) :: name
1011 logical :: found
1012 integer :: i
1013
1014 found = .false.
1015 do i = 1, shell%num_functions
1016 if (trim(shell%functions(i)%name) == trim(name)) then
1017 found = .true.
1018 return
1019 end if
1020 end do
1021 end function
1022
1023 function get_function_body(shell, name) result(body)
1024 type(shell_state_t), intent(in) :: shell
1025 character(len=*), intent(in) :: name
1026 type(string_t), allocatable :: body(:)
1027 integer :: i, j
1028
1029 do i = 1, shell%num_functions
1030 if (trim(shell%functions(i)%name) == trim(name)) then
1031 if (allocated(shell%functions(i)%body)) then
1032 allocate(body(shell%functions(i)%body_lines))
1033 do j = 1, shell%functions(i)%body_lines
1034 body(j)%str = shell%functions(i)%body(j)%str
1035 end do
1036 end if
1037 return
1038 end if
1039 end do
1040 end function
1041
1042 ! Array variable functions
1043 subroutine set_array_variable(shell, name, values, count)
1044 type(shell_state_t), intent(inout) :: shell
1045 character(len=*), intent(in) :: name
1046 character(len=*), intent(in) :: values(:)
1047 integer, intent(in) :: count
1048 integer :: i, k, empty_slot
1049
1050 empty_slot = -1
1051
1052 ! Check if variable already exists
1053 do i = 1, shell%num_variables
1054 if (trim(shell%variables(i)%name) == trim(name)) then
1055 if (allocated(shell%variables(i)%array_values)) deallocate(shell%variables(i)%array_values)
1056 allocate(shell%variables(i)%array_values(count))
1057 do k = 1, count
1058 shell%variables(i)%array_values(k)%str = trim(values(k))
1059 end do
1060 shell%variables(i)%array_size = count
1061 shell%variables(i)%is_array = .true.
1062 return
1063 end if
1064 end do
1065
1066 ! Find empty slot
1067 do i = 1, size(shell%variables)
1068 if (shell%variables(i)%name(1:1) == char(0) .or. trim(shell%variables(i)%name) == '') then
1069 empty_slot = i
1070 exit
1071 end if
1072 end do
1073
1074 ! Add new array variable
1075 if (empty_slot > 0) then
1076 shell%variables(empty_slot)%name = name
1077 shell%variables(empty_slot)%is_array = .true.
1078 shell%variables(empty_slot)%array_size = count
1079 if (allocated(shell%variables(empty_slot)%array_values)) deallocate(shell%variables(empty_slot)%array_values)
1080 allocate(shell%variables(empty_slot)%array_values(count))
1081 do k = 1, count
1082 shell%variables(empty_slot)%array_values(k)%str = trim(values(k))
1083 end do
1084 shell%num_variables = shell%num_variables + 1
1085 end if
1086 end subroutine
1087
1088 subroutine set_array_variable_string_t(shell, name, values, count)
1089 type(shell_state_t), intent(inout) :: shell
1090 character(len=*), intent(in) :: name
1091 type(string_t), intent(in) :: values(:)
1092 integer, intent(in) :: count
1093 integer :: i, k, empty_slot
1094
1095 empty_slot = -1
1096
1097 ! Check if variable already exists
1098 do i = 1, shell%num_variables
1099 if (trim(shell%variables(i)%name) == trim(name)) then
1100 if (allocated(shell%variables(i)%array_values)) deallocate(shell%variables(i)%array_values)
1101 allocate(shell%variables(i)%array_values(count))
1102 do k = 1, count
1103 shell%variables(i)%array_values(k)%str = values(k)%str
1104 end do
1105 shell%variables(i)%array_size = count
1106 shell%variables(i)%is_array = .true.
1107 return
1108 end if
1109 end do
1110
1111 ! Find empty slot
1112 do i = 1, size(shell%variables)
1113 if (shell%variables(i)%name(1:1) == char(0) .or. trim(shell%variables(i)%name) == '') then
1114 empty_slot = i
1115 exit
1116 end if
1117 end do
1118
1119 ! Add new array variable
1120 if (empty_slot > 0) then
1121 shell%variables(empty_slot)%name = name
1122 shell%variables(empty_slot)%is_array = .true.
1123 shell%variables(empty_slot)%array_size = count
1124 if (allocated(shell%variables(empty_slot)%array_values)) deallocate(shell%variables(empty_slot)%array_values)
1125 allocate(shell%variables(empty_slot)%array_values(count))
1126 do k = 1, count
1127 shell%variables(empty_slot)%array_values(k)%str = values(k)%str
1128 end do
1129 shell%num_variables = shell%num_variables + 1
1130 end if
1131 end subroutine
1132
1133 ! Set a single element in an array at the given index (1-indexed)
1134 subroutine set_array_element(shell, name, index, value)
1135 type(shell_state_t), intent(inout) :: shell
1136 character(len=*), intent(in) :: name
1137 integer, intent(in) :: index
1138 character(len=*), intent(in) :: value
1139 integer :: i, k, empty_slot, new_size
1140 type(string_t), allocatable :: temp_array(:)
1141
1142 ! Check if variable already exists
1143 do i = 1, shell%num_variables
1144 if (trim(shell%variables(i)%name) == trim(name)) then
1145 ! Variable exists - make sure it's an array
1146 if (.not. shell%variables(i)%is_array) then
1147 ! Convert to array
1148 shell%variables(i)%is_array = .true.
1149 if (allocated(shell%variables(i)%array_values)) deallocate(shell%variables(i)%array_values)
1150 allocate(shell%variables(i)%array_values(index))
1151 do k = 1, index
1152 shell%variables(i)%array_values(k)%str = ''
1153 end do
1154 shell%variables(i)%array_size = index
1155 else if (.not. allocated(shell%variables(i)%array_values)) then
1156 ! Array exists but not allocated (from declare -a)
1157 allocate(shell%variables(i)%array_values(index))
1158 do k = 1, index
1159 shell%variables(i)%array_values(k)%str = ''
1160 end do
1161 shell%variables(i)%array_size = index
1162 else if (index > shell%variables(i)%array_size) then
1163 ! Need to expand the array (sparse array support)
1164 new_size = index
1165 allocate(temp_array(new_size))
1166 do k = 1, new_size
1167 temp_array(k)%str = ''
1168 end do
1169 if (shell%variables(i)%array_size > 0 .and. allocated(shell%variables(i)%array_values)) then
1170 do k = 1, shell%variables(i)%array_size
1171 temp_array(k)%str = shell%variables(i)%array_values(k)%str
1172 end do
1173 end if
1174 if (allocated(shell%variables(i)%array_values)) deallocate(shell%variables(i)%array_values)
1175 allocate(shell%variables(i)%array_values(new_size))
1176 do k = 1, new_size
1177 shell%variables(i)%array_values(k)%str = temp_array(k)%str
1178 end do
1179 shell%variables(i)%array_size = new_size
1180 deallocate(temp_array)
1181 end if
1182
1183 ! Set the element
1184 shell%variables(i)%array_values(index)%str = value
1185 return
1186 end if
1187 end do
1188
1189 ! Variable doesn't exist - create new array
1190 empty_slot = -1
1191 do i = 1, size(shell%variables)
1192 if (shell%variables(i)%name(1:1) == char(0) .or. trim(shell%variables(i)%name) == '') then
1193 empty_slot = i
1194 exit
1195 end if
1196 end do
1197
1198 if (empty_slot > 0) then
1199 shell%variables(empty_slot)%name = name
1200 shell%variables(empty_slot)%is_array = .true.
1201 shell%variables(empty_slot)%array_size = index
1202 allocate(shell%variables(empty_slot)%array_values(index))
1203 do k = 1, index
1204 shell%variables(empty_slot)%array_values(k)%str = ''
1205 end do
1206 shell%variables(empty_slot)%array_values(index)%str = value
1207 shell%num_variables = shell%num_variables + 1
1208 end if
1209 end subroutine
1210
1211 function get_array_element(shell, name, index) result(value)
1212 type(shell_state_t), intent(in) :: shell
1213 character(len=*), intent(in) :: name
1214 integer, intent(in) :: index
1215 character(len=:), allocatable :: value
1216 integer :: i, actual_index
1217
1218 value = ''
1219
1220 do i = 1, shell%num_variables
1221 if (trim(shell%variables(i)%name) == trim(name) .and. shell%variables(i)%is_array) then
1222 actual_index = index
1223 ! Bash: negative indices count from end (-1 = last element)
1224 if (actual_index < 0) then
1225 actual_index = shell%variables(i)%array_size + actual_index + 1
1226 end if
1227 if (actual_index >= 1 .and. actual_index <= shell%variables(i)%array_size) then
1228 value = shell%variables(i)%array_values(actual_index)%str
1229 end if
1230 return
1231 end if
1232 end do
1233 end function
1234
1235 function get_array_all_elements(shell, name) result(result_str)
1236 type(shell_state_t), intent(in) :: shell
1237 character(len=*), intent(in) :: name
1238 character(len=4096) :: result_str
1239 integer :: i, j, pos
1240 logical :: first
1241
1242 result_str = ''
1243 pos = 1
1244 first = .true.
1245
1246 do i = 1, shell%num_variables
1247 if (trim(shell%variables(i)%name) == trim(name) .and. shell%variables(i)%is_array) then
1248 do j = 1, shell%variables(i)%array_size
1249 if (.not. allocated(shell%variables(i)%array_values(j)%str)) cycle
1250 if (len_trim(shell%variables(i)%array_values(j)%str) == 0) cycle
1251 if (.not. first) then
1252 result_str(pos:pos) = ' '
1253 pos = pos + 1
1254 end if
1255 first = .false.
1256 result_str(pos:pos+len_trim(shell%variables(i)%array_values(j)%str)-1) = &
1257 trim(shell%variables(i)%array_values(j)%str)
1258 pos = pos + len_trim(shell%variables(i)%array_values(j)%str)
1259 end do
1260 return
1261 end if
1262 end do
1263 end function
1264
1265 function get_array_size(shell, name) result(size)
1266 type(shell_state_t), intent(in) :: shell
1267 character(len=*), intent(in) :: name
1268 integer :: size
1269 integer :: i
1270
1271 size = 0
1272
1273 do i = 1, shell%num_variables
1274 if (trim(shell%variables(i)%name) == trim(name) .and. shell%variables(i)%is_array) then
1275 size = shell%variables(i)%array_size
1276 return
1277 end if
1278 end do
1279 end function
1280
1281 subroutine declare_associative_array(shell, name)
1282 type(shell_state_t), intent(inout) :: shell
1283 character(len=*), intent(in) :: name
1284
1285 integer :: i, empty_slot
1286
1287 empty_slot = -1
1288
1289 ! Check if variable already exists
1290 do i = 1, shell%num_variables
1291 if (trim(shell%variables(i)%name) == trim(name)) then
1292 ! Convert to associative array
1293 shell%variables(i)%is_assoc_array = .true.
1294 shell%variables(i)%is_array = .false.
1295 if (.not. allocated(shell%variables(i)%assoc_entries)) then
1296 allocate(shell%variables(i)%assoc_entries(50)) ! Initial size
1297 end if
1298 shell%variables(i)%assoc_size = 0
1299 return
1300 end if
1301 end do
1302
1303 ! Find empty slot
1304 do i = 1, size(shell%variables)
1305 if (shell%variables(i)%name(1:1) == char(0) .or. trim(shell%variables(i)%name) == '') then
1306 empty_slot = i
1307 exit
1308 end if
1309 end do
1310
1311 ! Add new associative array variable
1312 if (empty_slot > 0) then
1313 shell%variables(empty_slot)%name = name
1314 shell%variables(empty_slot)%value = ''
1315 shell%variables(empty_slot)%is_assoc_array = .true.
1316 shell%variables(empty_slot)%is_array = .false.
1317 allocate(shell%variables(empty_slot)%assoc_entries(50))
1318 shell%variables(empty_slot)%assoc_size = 0
1319 shell%num_variables = shell%num_variables + 1
1320 else
1321 write(error_unit, '(a)') 'declare: too many variables defined'
1322 end if
1323 end subroutine
1324
1325 subroutine set_assoc_array_value(shell, array_name, key, value)
1326 type(shell_state_t), intent(inout) :: shell
1327 character(len=*), intent(in) :: array_name, key, value
1328
1329 integer :: i, j
1330
1331 ! Find the associative array variable
1332 do i = 1, shell%num_variables
1333 if (trim(shell%variables(i)%name) == trim(array_name) .and. &
1334 shell%variables(i)%is_assoc_array) then
1335
1336 ! Check if key already exists
1337 do j = 1, shell%variables(i)%assoc_size
1338 if (trim(shell%variables(i)%assoc_entries(j)%key) == trim(key)) then
1339 call safe_assign_alloc_str(shell%variables(i)%assoc_entries(j)%value, &
1340 value, len_trim(value))
1341 return
1342 end if
1343 end do
1344
1345 ! Add new key-value pair — grow array if needed
1346 if (shell%variables(i)%assoc_size >= size(shell%variables(i)%assoc_entries)) then
1347 block
1348 type(assoc_array_entry_t), allocatable :: new_entries(:)
1349 integer :: old_size, new_size, k
1350 old_size = size(shell%variables(i)%assoc_entries)
1351 new_size = old_size * 2
1352 allocate(new_entries(new_size))
1353 do k = 1, old_size
1354 new_entries(k)%key = shell%variables(i)%assoc_entries(k)%key
1355 call safe_assign_alloc_str(new_entries(k)%value, &
1356 shell%variables(i)%assoc_entries(k)%value, &
1357 len_trim(shell%variables(i)%assoc_entries(k)%value))
1358 end do
1359 call move_alloc(new_entries, shell%variables(i)%assoc_entries)
1360 end block
1361 end if
1362 shell%variables(i)%assoc_size = shell%variables(i)%assoc_size + 1
1363 shell%variables(i)%assoc_entries(shell%variables(i)%assoc_size)%key = key
1364 call safe_assign_alloc_str( &
1365 shell%variables(i)%assoc_entries(shell%variables(i)%assoc_size)%value, &
1366 value, len_trim(value))
1367 return
1368 end if
1369 end do
1370
1371 write(error_unit, '(a)') 'associative array: ' // trim(array_name) // ' not declared'
1372 end subroutine
1373
1374 function get_assoc_array_value(shell, array_name, key) result(value)
1375 type(shell_state_t), intent(in) :: shell
1376 character(len=*), intent(in) :: array_name, key
1377 character(len=:), allocatable :: value
1378
1379 integer :: i, j
1380
1381 value = ''
1382
1383 ! Find the associative array variable
1384 do i = 1, shell%num_variables
1385 if (trim(shell%variables(i)%name) == trim(array_name) .and. &
1386 shell%variables(i)%is_assoc_array) then
1387
1388 ! Find the key
1389 do j = 1, shell%variables(i)%assoc_size
1390 if (trim(shell%variables(i)%assoc_entries(j)%key) == trim(key)) then
1391 value = shell%variables(i)%assoc_entries(j)%value
1392 return
1393 end if
1394 end do
1395 return ! Key not found, return empty string
1396 end if
1397 end do
1398 end function
1399
1400 subroutine get_assoc_array_keys(shell, array_name, keys, num_keys)
1401 type(shell_state_t), intent(in) :: shell
1402 character(len=*), intent(in) :: array_name
1403 character(len=256), intent(out) :: keys(:)
1404 integer, intent(out) :: num_keys
1405
1406 integer :: i, j
1407
1408 num_keys = 0
1409
1410 ! Find the associative array variable
1411 do i = 1, shell%num_variables
1412 if (trim(shell%variables(i)%name) == trim(array_name) .and. &
1413 shell%variables(i)%is_assoc_array) then
1414
1415 num_keys = min(shell%variables(i)%assoc_size, size(keys))
1416 do j = 1, num_keys
1417 keys(j) = shell%variables(i)%assoc_entries(j)%key
1418 end do
1419 return
1420 end if
1421 end do
1422 end subroutine
1423
1424 subroutine unset_assoc_array_key(shell, array_name, key)
1425 type(shell_state_t), intent(inout) :: shell
1426 character(len=*), intent(in) :: array_name, key
1427 integer :: i, j, k
1428
1429 do i = 1, shell%num_variables
1430 if (trim(shell%variables(i)%name) == trim(array_name) .and. &
1431 shell%variables(i)%is_assoc_array) then
1432 do j = 1, shell%variables(i)%assoc_size
1433 if (trim(shell%variables(i)%assoc_entries(j)%key) == trim(key)) then
1434 ! Shift remaining entries down
1435 do k = j, shell%variables(i)%assoc_size - 1
1436 shell%variables(i)%assoc_entries(k) = &
1437 shell%variables(i)%assoc_entries(k+1)
1438 end do
1439 shell%variables(i)%assoc_size = &
1440 shell%variables(i)%assoc_size - 1
1441 return
1442 end if
1443 end do
1444 return
1445 end if
1446 end do
1447 end subroutine
1448
1449 function is_associative_array(shell, name) result(is_assoc)
1450 type(shell_state_t), intent(in) :: shell
1451 character(len=*), intent(in) :: name
1452 logical :: is_assoc
1453
1454 integer :: i
1455
1456 is_assoc = .false.
1457 do i = 1, shell%num_variables
1458 if (trim(shell%variables(i)%name) == trim(name)) then
1459 is_assoc = shell%variables(i)%is_assoc_array
1460 return
1461 end if
1462 end do
1463 end function
1464
1465 ! POSIX parameter expansion implementation
1466 subroutine expand_parameter(param_expr, result, shell)
1467 character(len=*), intent(in) :: param_expr
1468 character(len=:), allocatable, intent(out) :: result
1469 type(shell_state_t), intent(inout) :: shell
1470
1471 character(len=256) :: param_name, default_value
1472 character(len=:), allocatable :: var_value
1473 character(len=:), allocatable :: expanded_pattern_buf
1474 integer :: colon_pos, dash_pos, plus_pos, eq_pos, question_pos
1475 integer :: percent_pos, hash_pos, percent2_pos, hash2_pos
1476 logical :: has_colon
1477
1478 result = ''
1479
1480 ! Check for various POSIX parameter expansion forms
1481 colon_pos = index(param_expr, ':')
1482 has_colon = colon_pos > 0
1483
1484 ! ${parameter:-word} or ${parameter-word}
1485 if (has_colon) then
1486 dash_pos = index(param_expr(colon_pos:), '-')
1487 if (dash_pos > 0) then
1488 dash_pos = dash_pos + colon_pos - 1
1489 param_name = param_expr(:colon_pos-1)
1490 default_value = param_expr(dash_pos+1:)
1491 end if
1492 else
1493 dash_pos = index(param_expr, '-')
1494 if (dash_pos > 0) then
1495 param_name = param_expr(:dash_pos-1)
1496 default_value = param_expr(dash_pos+1:)
1497 end if
1498 end if
1499
1500 if (dash_pos > 0) then
1501 var_value = get_shell_variable(shell, trim(param_name))
1502 if (has_colon) then
1503 ! ${parameter:-word} - use default if unset or null
1504 if (len_trim(var_value) == 0) then
1505 result = trim(default_value)
1506 else
1507 result = trim(var_value)
1508 end if
1509 else
1510 ! ${parameter-word} - use default if unset only
1511 if (len_trim(var_value) == 0 .and. .not. variable_exists(shell, trim(param_name))) then
1512 result = trim(default_value)
1513 else
1514 result = trim(var_value)
1515 end if
1516 end if
1517 return
1518 end if
1519
1520 ! ${parameter:=word} or ${parameter=word}
1521 if (has_colon) then
1522 eq_pos = index(param_expr(colon_pos:), '=')
1523 if (eq_pos > 0) then
1524 eq_pos = eq_pos + colon_pos - 1
1525 param_name = param_expr(:colon_pos-1)
1526 default_value = param_expr(eq_pos+1:)
1527 end if
1528 else
1529 eq_pos = index(param_expr, '=')
1530 if (eq_pos > 0) then
1531 param_name = param_expr(:eq_pos-1)
1532 default_value = param_expr(eq_pos+1:)
1533 end if
1534 end if
1535
1536 if (eq_pos > 0) then
1537 var_value = get_shell_variable(shell, trim(param_name))
1538 if (has_colon) then
1539 ! ${parameter:=word} - assign default if unset or null
1540 if (len_trim(var_value) == 0) then
1541 call set_shell_variable(shell, trim(param_name), trim(default_value))
1542 result = trim(default_value)
1543 else
1544 result = trim(var_value)
1545 end if
1546 else
1547 ! ${parameter=word} - assign default if unset only
1548 if (len_trim(var_value) == 0 .and. .not. variable_exists(shell, trim(param_name))) then
1549 call set_shell_variable(shell, trim(param_name), trim(default_value))
1550 result = trim(default_value)
1551 else
1552 result = trim(var_value)
1553 end if
1554 end if
1555 return
1556 end if
1557
1558 ! ${parameter:?word} or ${parameter?word}
1559 if (has_colon) then
1560 question_pos = index(param_expr(colon_pos:), '?')
1561 if (question_pos > 0) then
1562 question_pos = question_pos + colon_pos - 1
1563 param_name = param_expr(:colon_pos-1)
1564 default_value = param_expr(question_pos+1:)
1565 end if
1566 else
1567 question_pos = index(param_expr, '?')
1568 if (question_pos > 0) then
1569 param_name = param_expr(:question_pos-1)
1570 default_value = param_expr(question_pos+1:)
1571 end if
1572 end if
1573
1574 if (question_pos > 0) then
1575 var_value = get_shell_variable(shell, trim(param_name))
1576 if (has_colon) then
1577 ! ${parameter:?word} - error if unset or null
1578 if (len_trim(var_value) == 0) then
1579 ! TODO: Should write error and exit
1580 result = trim(param_name) // ': ' // trim(default_value)
1581 else
1582 result = trim(var_value)
1583 end if
1584 else
1585 ! ${parameter?word} - error if unset only
1586 if (len_trim(var_value) == 0 .and. .not. variable_exists(shell, trim(param_name))) then
1587 ! TODO: Should write error and exit
1588 result = trim(param_name) // ': ' // trim(default_value)
1589 else
1590 result = trim(var_value)
1591 end if
1592 end if
1593 return
1594 end if
1595
1596 ! ${parameter:+word} or ${parameter+word}
1597 if (has_colon) then
1598 plus_pos = index(param_expr(colon_pos:), '+')
1599 if (plus_pos > 0) then
1600 plus_pos = plus_pos + colon_pos - 1
1601 param_name = param_expr(:colon_pos-1)
1602 default_value = param_expr(plus_pos+1:)
1603 end if
1604 else
1605 plus_pos = index(param_expr, '+')
1606 if (plus_pos > 0) then
1607 param_name = param_expr(:plus_pos-1)
1608 default_value = param_expr(plus_pos+1:)
1609 end if
1610 end if
1611
1612 if (plus_pos > 0) then
1613 var_value = get_shell_variable(shell, trim(param_name))
1614 if (has_colon) then
1615 ! ${parameter:+word} - use word if set and not null
1616 if (len_trim(var_value) > 0) then
1617 result = trim(default_value)
1618 else
1619 result = ''
1620 end if
1621 else
1622 ! ${parameter+word} - use word if set
1623 if (variable_exists(shell, trim(param_name))) then
1624 result = trim(default_value)
1625 else
1626 result = ''
1627 end if
1628 end if
1629 return
1630 end if
1631
1632 ! ${parameter%word} - remove smallest suffix pattern
1633 percent_pos = index(param_expr, '%', back=.true.)
1634 if (percent_pos > 0 .and. param_expr(percent_pos-1:percent_pos-1) /= '%') then
1635 param_name = param_expr(:percent_pos-1)
1636 default_value = param_expr(percent_pos+1:)
1637 var_value = get_shell_variable(shell, trim(param_name))
1638 ! Expand simple $var in pattern
1639 expanded_pattern_buf = default_value
1640 if (index(default_value, '$') == 1) then
1641 ! Simple $var expansion (not ${} or $())
1642 if (len_trim(default_value) >= 2) then
1643 if (default_value(2:2) /= '{' .and. default_value(2:2) /= '(') then
1644 expanded_pattern_buf = get_shell_variable(shell, trim(default_value(2:)))
1645 end if
1646 end if
1647 end if
1648 call remove_suffix_pattern(trim(var_value), trim(expanded_pattern_buf), result, .false.)
1649 return
1650 end if
1651
1652 ! ${parameter%%word} - remove largest suffix pattern
1653 percent2_pos = index(param_expr, '%%')
1654 if (percent2_pos > 0) then
1655 param_name = param_expr(:percent2_pos-1)
1656 default_value = param_expr(percent2_pos+2:)
1657 var_value = get_shell_variable(shell, trim(param_name))
1658 ! Expand simple $var in pattern
1659 if (len_trim(default_value) > 1 .and. default_value(1:1) == '$' .and. &
1660 default_value(2:2) /= '{' .and. default_value(2:2) /= '(') then
1661 expanded_pattern_buf = get_shell_variable(shell, trim(default_value(2:)))
1662 else
1663 expanded_pattern_buf = default_value
1664 end if
1665 call remove_suffix_pattern(trim(var_value), trim(expanded_pattern_buf), result, .true.)
1666 return
1667 end if
1668
1669 ! ${parameter#word} - remove smallest prefix pattern
1670 hash_pos = index(param_expr, '#')
1671 if (hash_pos > 0 .and. param_expr(hash_pos:hash_pos+1) /= '##') then
1672 param_name = param_expr(:hash_pos-1)
1673 default_value = param_expr(hash_pos+1:)
1674 var_value = get_shell_variable(shell, trim(param_name))
1675 ! Expand simple $var in pattern
1676 if (len_trim(default_value) > 1 .and. default_value(1:1) == '$' .and. &
1677 default_value(2:2) /= '{' .and. default_value(2:2) /= '(') then
1678 expanded_pattern_buf = get_shell_variable(shell, trim(default_value(2:)))
1679 else
1680 expanded_pattern_buf = default_value
1681 end if
1682 call remove_prefix_pattern(trim(var_value), trim(expanded_pattern_buf), result, .false.)
1683 return
1684 end if
1685
1686 ! ${parameter##word} - remove largest prefix pattern
1687 hash2_pos = index(param_expr, '##')
1688 if (hash2_pos > 0) then
1689 param_name = param_expr(:hash2_pos-1)
1690 default_value = param_expr(hash2_pos+2:)
1691 var_value = get_shell_variable(shell, trim(param_name))
1692 ! Expand simple $var in pattern
1693 if (len_trim(default_value) > 1 .and. default_value(1:1) == '$' .and. &
1694 default_value(2:2) /= '{' .and. default_value(2:2) /= '(') then
1695 expanded_pattern_buf = get_shell_variable(shell, trim(default_value(2:)))
1696 else
1697 expanded_pattern_buf = default_value
1698 end if
1699 call remove_prefix_pattern(trim(var_value), trim(expanded_pattern_buf), result, .true.)
1700 return
1701 end if
1702
1703 ! Simple ${parameter} expansion
1704 result = trim(get_shell_variable(shell, trim(param_expr)))
1705 end subroutine
1706
1707 function variable_exists(shell, name) result(exists)
1708 type(shell_state_t), intent(in) :: shell
1709 character(len=*), intent(in) :: name
1710 logical :: exists
1711 integer :: i
1712
1713 exists = .false.
1714 do i = 1, shell%num_variables
1715 if (trim(shell%variables(i)%name) == trim(name)) then
1716 exists = .true.
1717 return
1718 end if
1719 end do
1720 end function
1721
1722 subroutine remove_suffix_pattern(value, pattern, result, largest)
1723 character(len=*), intent(in) :: value, pattern
1724 character(len=*), intent(out) :: result
1725 logical, intent(in) :: largest
1726
1727 integer :: i, match_pos
1728
1729 result = value
1730 match_pos = 0
1731
1732 ! Simple pattern matching - exact match only for now
1733 ! TODO: Add full glob pattern support
1734 if (largest) then
1735 ! Find rightmost match
1736 do i = len_trim(value), len_trim(pattern), -1
1737 if (value(i-len_trim(pattern)+1:i) == pattern) then
1738 match_pos = i - len_trim(pattern) + 1
1739 exit
1740 end if
1741 end do
1742 else
1743 ! Find leftmost match from the right
1744 do i = len_trim(value) - len_trim(pattern) + 1, 1, -1
1745 if (value(i:i+len_trim(pattern)-1) == pattern) then
1746 match_pos = i
1747 end if
1748 end do
1749 end if
1750
1751 if (match_pos > 0) then
1752 result = value(:match_pos-1)
1753 end if
1754 end subroutine
1755
1756 subroutine remove_prefix_pattern(value, pattern, result, largest)
1757 character(len=*), intent(in) :: value, pattern
1758 character(len=*), intent(out) :: result
1759 logical, intent(in) :: largest
1760
1761 integer :: i, match_pos, match_end
1762
1763 result = value
1764 match_pos = 0
1765 match_end = 0
1766
1767 ! Simple pattern matching - exact match only for now
1768 ! TODO: Add full glob pattern support
1769 if (largest) then
1770 ! Find rightmost match from the left
1771 do i = 1, len_trim(value) - len_trim(pattern) + 1
1772 if (value(i:i+len_trim(pattern)-1) == pattern) then
1773 match_pos = i
1774 match_end = i + len_trim(pattern) - 1
1775 end if
1776 end do
1777 else
1778 ! Find leftmost match
1779 do i = 1, len_trim(value) - len_trim(pattern) + 1
1780 if (value(i:i+len_trim(pattern)-1) == pattern) then
1781 match_pos = i
1782 match_end = i + len_trim(pattern) - 1
1783 exit
1784 end if
1785 end do
1786 end if
1787
1788 if (match_pos > 0) then
1789 result = value(match_end+1:)
1790 end if
1791 end subroutine
1792
1793 ! Positional parameter support functions
1794 subroutine set_positional_params(shell, params, count)
1795 type(shell_state_t), intent(inout) :: shell
1796 character(len=*), intent(in) :: params(:)
1797 integer, intent(in) :: count
1798 integer :: i, actual_count
1799
1800 actual_count = min(count, size(shell%positional_params))
1801 shell%num_positional = actual_count
1802
1803 do i = 1, actual_count
1804 shell%positional_params(i)%str = params(i)
1805 end do
1806
1807 ! Clear any remaining parameters
1808 do i = actual_count + 1, size(shell%positional_params)
1809 shell%positional_params(i)%str = ''
1810 end do
1811 end subroutine
1812
1813 subroutine get_all_positional_params(shell, result, as_single_word)
1814 type(shell_state_t), intent(in) :: shell
1815 character(len=*), intent(out) :: result
1816 logical, intent(in) :: as_single_word
1817 integer :: i, pos
1818 character(len=1) :: separator
1819
1820 result = ''
1821 if (shell%num_positional == 0) return
1822
1823 if (as_single_word) then
1824 ! Use first character of IFS as separator for $*
1825 ! POSIX: If IFS is empty (set to ""), no separator is used
1826 if (len_trim(shell%ifs) > 0) then
1827 separator = shell%ifs(1:1)
1828 else
1829 separator = char(0) ! Use NUL to indicate no separator
1830 end if
1831 else
1832 ! Use space for $@ (will be properly quoted during expansion)
1833 separator = ' '
1834 end if
1835
1836 pos = 1
1837 do i = 1, shell%num_positional
1838 if (i > 1 .and. separator /= char(0)) then
1839 result(pos:pos) = separator
1840 pos = pos + 1
1841 end if
1842 result(pos:pos+len_trim(shell%positional_params(i)%str)-1) = trim(shell%positional_params(i)%str)
1843 pos = pos + len_trim(shell%positional_params(i)%str)
1844 end do
1845 end subroutine
1846
1847 subroutine shift_positional_params(shell, count)
1848 type(shell_state_t), intent(inout) :: shell
1849 integer, intent(in) :: count
1850 integer :: i, shift_count
1851
1852 shift_count = min(count, shell%num_positional)
1853
1854 ! Shift parameters left
1855 do i = 1, shell%num_positional - shift_count
1856 shell%positional_params(i)%str = shell%positional_params(i + shift_count)%str
1857 end do
1858
1859 ! Clear the shifted parameters
1860 do i = shell%num_positional - shift_count + 1, shell%num_positional
1861 shell%positional_params(i)%str = ''
1862 end do
1863
1864 shell%num_positional = shell%num_positional - shift_count
1865 end subroutine
1866
1867 function is_numeric(str) result(is_num)
1868 character(len=*), intent(in) :: str
1869 logical :: is_num
1870 integer :: i
1871
1872 is_num = .false.
1873 if (len_trim(str) == 0) return
1874
1875 do i = 1, len_trim(str)
1876 if (str(i:i) < '0' .or. str(i:i) > '9') return
1877 end do
1878
1879 is_num = .true.
1880 end function
1881
1882 function string_to_int(str) result(int_val)
1883 character(len=*), intent(in) :: str
1884 integer :: int_val, iostat
1885
1886 read(str, *, iostat=iostat) int_val
1887 if (iostat /= 0) int_val = 0 ! Error reading, return 0
1888 end function
1889
1890 ! Helper functions for special variables
1891 function get_shell_option_flags(shell) result(flags)
1892 type(shell_state_t), intent(in) :: shell
1893 character(len=256) :: flags
1894 integer :: pos
1895
1896 flags = ''
1897 pos = 1
1898
1899 ! Build option flags string from shell options
1900 ! Order follows bash convention for common flags: h, i, m, B, H, s, then others
1901 ! h for hashall (enabled by default in most shells)
1902 flags(pos:pos) = 'h'
1903 pos = pos + 1
1904 if (shell%is_interactive) then
1905 flags(pos:pos) = 'i'
1906 pos = pos + 1
1907 end if
1908 if (shell%option_monitor) then
1909 flags(pos:pos) = 'm'
1910 pos = pos + 1
1911 end if
1912 ! B for braceexpand (bash extension, enabled by default)
1913 flags(pos:pos) = 'B'
1914 pos = pos + 1
1915 ! c flag when running in command mode (-c)
1916 if (shell%in_command_mode) then
1917 flags(pos:pos) = 'c'
1918 pos = pos + 1
1919 end if
1920 if (shell%option_allexport) then
1921 flags(pos:pos) = 'a'
1922 pos = pos + 1
1923 end if
1924 if (shell%option_errexit) then
1925 flags(pos:pos) = 'e'
1926 pos = pos + 1
1927 end if
1928 if (shell%option_noglob) then
1929 flags(pos:pos) = 'f'
1930 pos = pos + 1
1931 end if
1932 if (shell%option_nounset) then
1933 flags(pos:pos) = 'u'
1934 pos = pos + 1
1935 end if
1936 if (shell%option_verbose) then
1937 flags(pos:pos) = 'v'
1938 pos = pos + 1
1939 end if
1940 if (shell%option_xtrace) then
1941 flags(pos:pos) = 'x'
1942 pos = pos + 1
1943 end if
1944 if (shell%option_noclobber) then
1945 flags(pos:pos) = 'C'
1946 pos = pos + 1
1947 end if
1948 end function
1949
1950 subroutine get_random_number(value)
1951 character(len=*), intent(out) :: value
1952 real :: rand_val
1953 integer :: rand_int
1954
1955 call random_number(rand_val)
1956 rand_int = int(rand_val * 32768.0)
1957 write(value, '(i15)') rand_int
1958 end subroutine
1959
1960 subroutine get_seconds_since_start(shell, value)
1961 type(shell_state_t), intent(in) :: shell
1962 character(len=*), intent(out) :: value
1963 integer :: current_time, elapsed
1964
1965 ! Get current time
1966 call system_clock(current_time)
1967
1968 ! Calculate elapsed seconds
1969 if (shell%shell_start_time > 0) then
1970 elapsed = (current_time - shell%shell_start_time) / 1000 ! Assuming milliseconds
1971 else
1972 elapsed = 0
1973 end if
1974
1975 write(value, '(i15)') elapsed
1976 end subroutine
1977
1978 function get_random_int() result(rand_int)
1979 integer :: rand_int
1980 real :: rand_val
1981 call random_number(rand_val)
1982 rand_int = int(rand_val * 32768.0)
1983 end function
1984
1985 function get_elapsed_seconds(shell) result(elapsed)
1986 type(shell_state_t), intent(in) :: shell
1987 integer :: elapsed, current_time
1988 call system_clock(current_time)
1989 if (shell%shell_start_time > 0) then
1990 elapsed = (current_time - shell%shell_start_time) / 1000
1991 else
1992 elapsed = 0
1993 end if
1994 end function
1995
1996 ! Get the actual stored length of a variable (for ${#var} expansion)
1997 ! Returns -1 if variable not found or doesn't have stored length
1998 subroutine get_variable_length(shell, name, length)
1999 type(shell_state_t), intent(in) :: shell
2000 character(len=*), intent(in) :: name
2001 integer, intent(out) :: length
2002 integer :: i, depth
2003
2004 length = -1
2005
2006 ! First check local variables (innermost scope first)
2007 if (shell%function_depth > 0) then
2008 do depth = shell%function_depth, 1, -1
2009 if (depth <= size(shell%local_var_counts)) then
2010 do i = 1, shell%local_var_counts(depth)
2011 if (trim(shell%local_vars(depth, i)%name) == trim(name)) then
2012 length = shell%local_vars(depth, i)%value_len
2013 return
2014 end if
2015 end do
2016 end if
2017 end do
2018 end if
2019
2020 ! Check special prompt variables
2021 select case (trim(name))
2022 case ('PS1')
2023 length = shell%ps1_len
2024 return
2025 case ('PS2')
2026 length = shell%ps2_len
2027 return
2028 case ('PS3')
2029 length = shell%ps3_len
2030 return
2031 case ('PS4')
2032 length = shell%ps4_len
2033 return
2034 end select
2035
2036 ! Check regular shell variables
2037 do i = 1, shell%num_variables
2038 if (trim(shell%variables(i)%name) == trim(name)) then
2039 length = shell%variables(i)%value_len
2040 return
2041 end if
2042 end do
2043 end subroutine
2044
2045 ! Strip surrounding quotes (single or double) from a string
2046 ! Preserves trailing spaces within quotes
2047 subroutine strip_quotes(str)
2048 character(len=*), intent(inout) :: str
2049 integer :: i, search_end, closing_quote_pos, content_len
2050 character(len=len(str)) :: temp
2051 character(len=1) :: quote_char
2052
2053 if (len_trim(str) < 2) return
2054
2055 ! Check if string starts with a quote
2056 if (str(1:1) /= "'" .and. str(1:1) /= '"') return
2057
2058 quote_char = str(1:1)
2059
2060 ! Search backwards to find closing quote (use len_trim to avoid padding)
2061 closing_quote_pos = 0
2062 search_end = len_trim(str)
2063 do i = search_end, 2, -1
2064 if (str(i:i) == quote_char) then
2065 closing_quote_pos = i
2066 exit
2067 end if
2068 end do
2069
2070 ! If we found a matching closing quote, extract the content (preserving all characters including trailing spaces)
2071 if (closing_quote_pos > 1) then
2072 content_len = closing_quote_pos - 2
2073 ! Save the original string first
2074 temp = str
2075 ! Clear the output string
2076 str = repeat(' ', len(str))
2077 ! Copy character by character from positions 2 to closing_quote_pos-1
2078 ! This preserves ALL characters including trailing spaces
2079 do i = 1, content_len
2080 str(i:i) = temp(i+1:i+1)
2081 end do
2082 end if
2083 end subroutine
2084
2085 ! Check if nounset option is enabled and handle undefined variable
2086 ! Moved from shell_options to break circular dependency
2087 function check_nounset(shell, var_name) result(should_error)
2088 type(shell_state_t), intent(in) :: shell
2089 character(len=*), intent(in) :: var_name
2090 logical :: should_error
2091
2092 should_error = shell%option_nounset
2093 if (should_error) then
2094 write(error_unit, '(a)') 'fortsh: ' // trim(var_name) // ': unbound variable'
2095 end if
2096 end function
2097
2098 end module variables