Fortran · 19258 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: shell_options
3 ! Purpose: Shell options management (set, shopt, POSIX compliance)
4 ! ==============================================================================
5 module shell_options
6 use shell_types
7 use variables, only: set_shell_variable
8 use system_interface, only: get_pid, get_ppid
9 use readline, only: set_global_editing_mode, set_global_fuzzy_complete
10 use iso_fortran_env, only: output_unit, error_unit
11 use io_helpers, only: write_stdout
12 implicit none
13
14 contains
15
16 ! Initialize shell special variables and options
17 subroutine initialize_shell_options(shell)
18 type(shell_state_t), intent(inout) :: shell
19
20 ! Set special process variables
21 shell%shell_pid = get_pid()
22 shell%parent_pid = get_ppid()
23 shell%shell_name = 'fortsh'
24
25 ! Set default POSIX options (conservative defaults)
26 shell%option_errexit = .false.
27 shell%option_nounset = .false.
28 shell%option_pipefail = .false.
29 shell%option_verbose = .false.
30 shell%option_xtrace = .false.
31 shell%option_noclobber = .false.
32 shell%option_monitor = .false. ! Job control only for interactive mode (set later)
33 shell%option_allexport = .false.
34 shell%option_noglob = .false.
35 shell%option_vi = .false. ! Emacs mode by default
36
37 ! Set default bash-style options
38 shell%shopt_nullglob = .false.
39 shell%shopt_failglob = .false.
40 shell%shopt_globstar = .false.
41 shell%shopt_nocaseglob = .false.
42 shell%shopt_nocasematch = .false.
43 shell%shopt_extglob = .false.
44 shell%shopt_dotglob = .false.
45 shell%shopt_expand_aliases = .false.
46 end subroutine
47
48 ! Handle 'set' builtin command for POSIX options
49 subroutine builtin_set(cmd, shell)
50 type(command_t), intent(in) :: cmd
51 type(shell_state_t), intent(inout) :: shell
52
53 character(len=256) :: option_str, option_name, param_idx_str
54 integer :: i, arg_len, param_idx
55 logical :: enable_option, setting_positional
56
57 if (cmd%num_tokens == 1) then
58 ! Show all variables (simplified)
59 call show_shell_variables(shell)
60 return
61 end if
62
63 setting_positional = .false.
64 i = 2
65 do while (i <= cmd%num_tokens)
66 option_str = trim(cmd%tokens(i))
67 arg_len = len_trim(option_str)
68
69 ! Check for '--' which signals end of options and start of positional parameters
70 if (option_str == '--') then
71 setting_positional = .true.
72 i = i + 1
73 ! Clear existing positional parameters
74 shell%num_positional = 0
75 ! Set remaining arguments as positional parameters
76 param_idx = 1
77 do while (i <= cmd%num_tokens .and. param_idx <= 50)
78 shell%positional_params(param_idx)%str = trim(cmd%tokens(i))
79 write(param_idx_str, '(I0)') param_idx
80 call set_shell_variable(shell, trim(param_idx_str), trim(cmd%tokens(i)))
81 param_idx = param_idx + 1
82 i = i + 1
83 end do
84 shell%num_positional = param_idx - 1
85 write(param_idx_str, '(I0)') shell%num_positional
86 call set_shell_variable(shell, '#', trim(param_idx_str))
87 exit
88 end if
89
90 ! If argument doesn't start with - or +, treat rest as positional parameters
91 if (arg_len >= 1 .and. option_str(1:1) /= '-' .and. option_str(1:1) /= '+') then
92 setting_positional = .true.
93 shell%num_positional = 0
94 param_idx = 1
95 do while (i <= cmd%num_tokens .and. param_idx <= 50)
96 shell%positional_params(param_idx)%str = trim(cmd%tokens(i))
97 write(param_idx_str, '(I0)') param_idx
98 call set_shell_variable(shell, trim(param_idx_str), trim(cmd%tokens(i)))
99 param_idx = param_idx + 1
100 i = i + 1
101 end do
102 shell%num_positional = param_idx - 1
103 write(param_idx_str, '(I0)') shell%num_positional
104 call set_shell_variable(shell, '#', trim(param_idx_str))
105 exit
106 end if
107
108 if (arg_len < 2) then
109 i = i + 1
110 cycle
111 end if
112
113 ! Check if enabling (+) or disabling (-) option
114 if (option_str(1:1) == '-') then
115 enable_option = .true.
116 option_name = option_str(2:arg_len)
117 else if (option_str(1:1) == '+') then
118 enable_option = .false.
119 option_name = option_str(2:arg_len)
120 else
121 write(error_unit, '(a)') 'set: invalid option format: ' // trim(option_str)
122 shell%last_exit_status = 1
123 i = i + 1
124 cycle
125 end if
126
127 ! Handle options — iterate over each character to support combined flags like -eo
128 block
129 integer :: fi
130 logical :: had_error
131 character(len=256) :: long_opt_name
132 had_error = .false.
133 fi = 1
134 do while (fi <= len_trim(option_name))
135 select case (option_name(fi:fi))
136 case ('e')
137 shell%option_errexit = enable_option
138 case ('u')
139 shell%option_nounset = enable_option
140 case ('n')
141 shell%option_noexec = enable_option
142 case ('v')
143 shell%option_verbose = enable_option
144 case ('x')
145 shell%option_xtrace = enable_option
146 case ('C')
147 shell%option_noclobber = enable_option
148 case ('m')
149 shell%option_monitor = enable_option
150 case ('a')
151 shell%option_allexport = enable_option
152 case ('f')
153 shell%option_noglob = enable_option
154 case ('o')
155 ! -o requires the next argument as the option name
156 if (i >= cmd%num_tokens) then
157 call list_shell_options(shell)
158 shell%last_exit_status = 0
159 else
160 i = i + 1
161 long_opt_name = trim(cmd%tokens(i))
162 select case (trim(long_opt_name))
163 case ('allexport')
164 shell%option_allexport = enable_option
165 case ('braceexpand')
166 shell%option_braceexpand = enable_option
167 case ('emacs')
168 shell%option_emacs = enable_option
169 shell%option_vi = .not. enable_option
170 call set_global_editing_mode(.not. enable_option)
171 case ('errexit')
172 shell%option_errexit = enable_option
173 case ('errtrace')
174 shell%option_errtrace = enable_option
175 case ('functrace')
176 shell%option_functrace = enable_option
177 case ('fuzzy-complete')
178 shell%option_fuzzy_complete = enable_option
179 call set_global_fuzzy_complete(enable_option)
180 case ('hashall')
181 shell%option_hashall = enable_option
182 case ('histexpand')
183 shell%option_histexpand = enable_option
184 case ('history')
185 shell%option_history = enable_option
186 case ('ignoreeof')
187 shell%option_ignoreeof = enable_option
188 case ('interactive-comments')
189 shell%option_interactive_comments = enable_option
190 case ('keyword')
191 shell%option_keyword = enable_option
192 case ('monitor')
193 shell%option_monitor = enable_option
194 case ('noclobber')
195 shell%option_noclobber = enable_option
196 case ('noexec')
197 shell%option_noexec = enable_option
198 case ('noglob')
199 shell%option_noglob = enable_option
200 case ('nolog')
201 shell%option_nolog = enable_option
202 case ('notify')
203 shell%option_notify = enable_option
204 case ('nounset')
205 shell%option_nounset = enable_option
206 case ('onecmd')
207 shell%option_onecmd = enable_option
208 case ('physical')
209 shell%option_physical = enable_option
210 case ('pipefail')
211 shell%option_pipefail = enable_option
212 case ('posix')
213 shell%option_posix = enable_option
214 case ('privileged')
215 shell%option_privileged = enable_option
216 case ('verbose')
217 shell%option_verbose = enable_option
218 case ('vi')
219 shell%option_vi = enable_option
220 shell%option_emacs = .not. enable_option
221 call set_global_editing_mode(enable_option)
222 case ('xtrace')
223 shell%option_xtrace = enable_option
224 case default
225 write(error_unit, '(a)') 'set: unknown option: ' // trim(long_opt_name)
226 shell%last_exit_status = 1
227 end select
228 end if
229 case default
230 write(error_unit, '(a)') 'set: unknown option: -' // option_name(fi:fi)
231 had_error = .true.
232 shell%last_exit_status = 1
233 end select
234 fi = fi + 1
235 end do
236 end block
237
238 ! Always increment
239 i = i + 1
240 end do
241
242 shell%last_exit_status = 0
243 end subroutine
244
245 ! Handle 'shopt' builtin command for bash-style options
246 subroutine builtin_shopt(cmd, shell)
247 type(command_t), intent(in) :: cmd
248 type(shell_state_t), intent(inout) :: shell
249
250 character(len=256) :: option_name, flag
251 integer :: i
252 logical :: show_all = .false., enable_option = .true.
253
254 if (cmd%num_tokens == 1) then
255 show_all = .true.
256 end if
257
258 i = 2
259 do while (i <= cmd%num_tokens)
260 flag = trim(cmd%tokens(i))
261
262 if (flag == '-s') then
263 enable_option = .true.
264 else if (flag == '-u') then
265 enable_option = .false.
266 else if (flag == '-p') then
267 show_all = .true.
268 else
269 option_name = trim(flag)
270 call set_shopt_option(shell, option_name, enable_option)
271 end if
272
273 i = i + 1
274 end do
275
276 if (show_all) then
277 call show_shopt_options(shell)
278 end if
279
280 shell%last_exit_status = 0
281 end subroutine
282
283 ! Set a shopt option
284 subroutine set_shopt_option(shell, option_name, enable)
285 type(shell_state_t), intent(inout) :: shell
286 character(len=*), intent(in) :: option_name
287 logical, intent(in) :: enable
288
289 select case (trim(option_name))
290 case ('nullglob')
291 shell%shopt_nullglob = enable
292 case ('failglob')
293 shell%shopt_failglob = enable
294 case ('globstar')
295 shell%shopt_globstar = enable
296 case ('nocaseglob')
297 shell%shopt_nocaseglob = enable
298 case ('nocasematch')
299 shell%shopt_nocasematch = enable
300 case ('extglob')
301 shell%shopt_extglob = enable
302 case ('dotglob')
303 shell%shopt_dotglob = enable
304 case ('expand_aliases')
305 shell%shopt_expand_aliases = enable
306 case default
307 write(error_unit, '(a)') 'shopt: unknown option: ' // trim(option_name)
308 shell%last_exit_status = 1
309 end select
310 end subroutine
311
312 ! Show all shopt options
313 subroutine show_shopt_options(shell)
314 type(shell_state_t), intent(in) :: shell
315
316 write(output_unit, '(a,a)') 'shopt ', merge('-s nullglob ', '-u nullglob ', shell%shopt_nullglob)
317 write(output_unit, '(a,a)') 'shopt ', merge('-s failglob ', '-u failglob ', shell%shopt_failglob)
318 write(output_unit, '(a,a)') 'shopt ', merge('-s globstar ', '-u globstar ', shell%shopt_globstar)
319 write(output_unit, '(a,a)') 'shopt ', merge('-s nocaseglob ', '-u nocaseglob ', shell%shopt_nocaseglob)
320 write(output_unit, '(a,a)') 'shopt ', merge('-s nocasematch ', '-u nocasematch ', shell%shopt_nocasematch)
321 write(output_unit, '(a,a)') 'shopt ', merge('-s extglob ', '-u extglob ', shell%shopt_extglob)
322 write(output_unit, '(a,a)') 'shopt ', merge('-s dotglob ', '-u dotglob ', shell%shopt_dotglob)
323 write(output_unit, '(a,a)') 'shopt ', merge('-s expand_aliases', '-u expand_aliases', shell%shopt_expand_aliases)
324 end subroutine
325
326 ! Show shell variables (simplified version for 'set' without args)
327 ! Uses write_stdout (C-level fd write) instead of Fortran output_unit
328 ! so output respects dup2 redirections on all compilers (flang-new caches fd)
329 subroutine show_shell_variables(shell)
330 type(shell_state_t), intent(in) :: shell
331 integer :: i
332 character(len=64) :: num_str
333
334 call write_stdout('# Shell variables:')
335 do i = 1, shell%num_variables
336 if (shell%variables(i)%name(1:1) /= char(0) .and. trim(shell%variables(i)%name) /= '') then
337 if (shell%variables(i)%is_array) then
338 call write_stdout(trim(shell%variables(i)%name) // '=(array)')
339 else if (shell%variables(i)%is_assoc_array) then
340 call write_stdout(trim(shell%variables(i)%name) // '=(associative array)')
341 else
342 block
343 character(len=:), allocatable :: val
344 logical :: needs_quote
345 integer :: vi
346 val = trim(shell%variables(i)%value)
347 needs_quote = .false.
348 do vi = 1, len(val)
349 select case(val(vi:vi))
350 case(' ', char(9), char(10), '"', "'", '\', '$', '`', &
351 '!', '(', ')', '{', '}', '[', ']', '|', '&', ';', &
352 '<', '>', '?', '*', '#', '~')
353 needs_quote = .true.
354 exit
355 end select
356 end do
357 if (needs_quote) then
358 call write_stdout(trim(shell%variables(i)%name) // '=' // "'" // val // "'")
359 else
360 call write_stdout(trim(shell%variables(i)%name) // '=' // val)
361 end if
362 end block
363 end if
364 end if
365 end do
366
367 call write_stdout('# Special variables:')
368 write(num_str, '(i15)') shell%shell_pid
369 call write_stdout('$$=' // trim(adjustl(num_str)))
370 write(num_str, '(i15)') shell%last_bg_pid
371 call write_stdout('$!=' // trim(adjustl(num_str)))
372 call write_stdout('$0=' // trim(shell%shell_name))
373 write(num_str, '(i15)') shell%parent_pid
374 call write_stdout('$PPID=' // trim(adjustl(num_str)))
375 write(num_str, '(i15)') shell%last_exit_status
376 call write_stdout('$?=' // trim(adjustl(num_str)))
377 end subroutine
378
379 ! Check if errexit option is enabled and handle command failure
380 subroutine check_errexit(shell, exit_status)
381 type(shell_state_t), intent(inout) :: shell
382 integer, intent(in) :: exit_status
383
384 ! POSIX: Don't trigger errexit in these contexts:
385 ! - During if/while/until condition evaluation (evaluating_condition flag)
386 ! - In AND-OR lists (&&, ||)
387 ! - In negated pipelines (!)
388 ! - In command substitution
389 if (shell%evaluating_condition) return
390 if (shell%in_and_or_list) return
391 if (shell%in_negation) return
392 if (shell%in_command_substitution) return
393
394 if (shell%option_errexit .and. exit_status /= 0) then
395 ! POSIX: errexit exits silently (no message)
396 shell%running = .false.
397 shell%last_exit_status = exit_status
398 end if
399 end subroutine
400
401 ! Check if nounset option is enabled and handle undefined variable
402 function check_nounset(shell, var_name) result(should_error)
403 type(shell_state_t), intent(in) :: shell
404 character(len=*), intent(in) :: var_name
405 logical :: should_error
406
407 should_error = shell%option_nounset
408 if (should_error) then
409 write(error_unit, '(a)') 'fortsh: ' // trim(var_name) // ': unbound variable'
410 end if
411 end function
412
413 ! Trace command execution if xtrace is enabled
414 subroutine trace_command(shell, command_line)
415 use prompt_formatting, only: expand_prompt
416 use iso_c_binding, only: c_size_t, c_loc, c_char
417 use system_interface, only: c_write
418 type(shell_state_t), intent(inout) :: shell
419 character(len=*), intent(in) :: command_line
420 character(len=:), allocatable :: expanded_ps4
421 character(len=2048) :: trace_line
422 integer :: ps4_actual_len, trace_len
423 character(kind=c_char), target, allocatable :: c_trace(:)
424 integer(c_size_t) :: bytes_written
425 integer :: i
426
427 if (shell%option_xtrace) then
428 ! Expand PS4 prompt (supports escape sequences like \h, \w, etc.)
429 expanded_ps4 = expand_prompt(shell%ps4, shell, shell%ps4_len)
430 ! Don't trim PS4 - it typically has a trailing space (e.g., '+ ')
431 ps4_actual_len = shell%ps4_len
432 if (ps4_actual_len > len(expanded_ps4)) ps4_actual_len = len_trim(expanded_ps4)
433
434 ! Build trace line
435 trace_line = expanded_ps4(1:ps4_actual_len) // trim(command_line)
436 trace_len = len_trim(trace_line)
437
438 ! Write to original stderr (shell's stderr, not affected by per-command redirections)
439 ! This matches bash behavior where xtrace goes to shell's stderr
440 allocate(c_trace(trace_len + 1))
441 do i = 1, trace_len
442 c_trace(i) = trace_line(i:i)
443 end do
444 c_trace(trace_len + 1) = char(10) ! newline
445
446 bytes_written = c_write(shell%original_stderr_fd, c_loc(c_trace), int(trace_len + 1, c_size_t))
447 deallocate(c_trace)
448 end if
449 end subroutine
450
451 ! List all shell options (for set -o)
452 subroutine list_shell_options(shell)
453 type(shell_state_t), intent(in) :: shell
454
455 ! Print each option with its current state (on/off), alphabetically sorted
456 call print_option('allexport', shell%option_allexport)
457 call print_option('braceexpand', shell%option_braceexpand)
458 call print_option('emacs', shell%option_emacs)
459 call print_option('errexit', shell%option_errexit)
460 call print_option('errtrace', shell%option_errtrace)
461 call print_option('functrace', shell%option_functrace)
462 call print_option('hashall', shell%option_hashall)
463 call print_option('histexpand', shell%option_histexpand)
464 call print_option('history', shell%option_history)
465 call print_option('ignoreeof', shell%option_ignoreeof)
466 call print_option('interactive-comments', shell%option_interactive_comments)
467 call print_option('keyword', shell%option_keyword)
468 call print_option('monitor', shell%option_monitor)
469 call print_option('noclobber', shell%option_noclobber)
470 call print_option('noexec', shell%option_noexec)
471 call print_option('noglob', shell%option_noglob)
472 call print_option('nolog', shell%option_nolog)
473 call print_option('notify', shell%option_notify)
474 call print_option('nounset', shell%option_nounset)
475 call print_option('onecmd', shell%option_onecmd)
476 call print_option('physical', shell%option_physical)
477 call print_option('pipefail', shell%option_pipefail)
478 call print_option('posix', shell%option_posix)
479 call print_option('privileged', shell%option_privileged)
480 call print_option('verbose', shell%option_verbose)
481 call print_option('vi', shell%option_vi)
482 call print_option('xtrace', shell%option_xtrace)
483 end subroutine
484
485 ! Helper to print an option with proper formatting
486 subroutine print_option(name, value)
487 character(len=*), intent(in) :: name
488 logical, intent(in) :: value
489 character(len=32) :: status
490
491 if (value) then
492 status = 'on'
493 else
494 status = 'off'
495 end if
496 write(output_unit, '(a,a,a)') name, ' ', trim(status)
497 end subroutine
498
499 end module shell_options