Fortran · 40364 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: builtins (Extended with job control)
3 ! ==============================================================================
4 module builtins
5 use shell_types
6 use system_interface
7 use job_control
8 use test_builtin
9 use readline
10 use shell_config
11 use aliases
12 use shell_options
13 use command_builtin, only: find_command_in_path
14 use performance
15 use parser
16 use coprocess
17 use substitution
18 use iso_fortran_env, only: output_unit, error_unit
19 implicit none
20
21 contains
22
23 function is_builtin(cmd_name) result(is_built)
24 character(len=*), intent(in) :: cmd_name
25 logical :: is_built
26
27 is_built = (trim(cmd_name) == 'exit' .or. &
28 trim(cmd_name) == 'cd' .or. &
29 trim(cmd_name) == 'pwd' .or. &
30 trim(cmd_name) == 'export' .or. &
31 trim(cmd_name) == 'echo' .or. &
32 trim(cmd_name) == 'jobs' .or. &
33 trim(cmd_name) == 'fg' .or. &
34 trim(cmd_name) == 'bg' .or. &
35 trim(cmd_name) == 'source' .or. &
36 trim(cmd_name) == '.' .or. &
37 trim(cmd_name) == 'history' .or. &
38 trim(cmd_name) == 'kill' .or. &
39 trim(cmd_name) == 'wait' .or. &
40 trim(cmd_name) == 'trap' .or. &
41 trim(cmd_name) == 'config' .or. &
42 trim(cmd_name) == 'alias' .or. &
43 trim(cmd_name) == 'unalias' .or. &
44 trim(cmd_name) == 'help' .or. &
45 trim(cmd_name) == 'perf' .or. &
46 trim(cmd_name) == 'memory' .or. &
47 trim(cmd_name) == 'rawtest' .or. &
48 trim(cmd_name) == 'defun' .or. &
49 trim(cmd_name) == 'set' .or. &
50 trim(cmd_name) == 'shopt' .or. &
51 trim(cmd_name) == 'type' .or. &
52 trim(cmd_name) == 'unset' .or. &
53 trim(cmd_name) == 'readonly' .or. &
54 trim(cmd_name) == 'shift' .or. &
55 trim(cmd_name) == 'break' .or. &
56 trim(cmd_name) == 'continue' .or. &
57 trim(cmd_name) == 'return' .or. &
58 trim(cmd_name) == 'exec' .or. &
59 trim(cmd_name) == 'eval' .or. &
60 trim(cmd_name) == 'hash' .or. &
61 trim(cmd_name) == 'umask' .or. &
62 trim(cmd_name) == 'ulimit' .or. &
63 trim(cmd_name) == 'times' .or. &
64 is_test_command(cmd_name))
65 end function
66
67 subroutine execute_builtin(cmd, shell)
68 type(command_t), intent(in) :: cmd
69 type(shell_state_t), intent(inout) :: shell
70
71 select case(trim(cmd%tokens(1)))
72 case('exit')
73 call builtin_exit(cmd, shell)
74 case('cd')
75 call builtin_cd(cmd, shell)
76 case('pwd')
77 call builtin_pwd(cmd, shell)
78 case('export')
79 call builtin_export(cmd, shell)
80 case('echo')
81 call builtin_echo(cmd, shell)
82 case('jobs')
83 call builtin_jobs(cmd, shell)
84 case('fg')
85 call builtin_fg(cmd, shell)
86 case('bg')
87 call builtin_bg(cmd, shell)
88 case('source', '.')
89 call builtin_source(cmd, shell)
90 case('history')
91 call builtin_history(cmd, shell)
92 case('kill')
93 call builtin_kill(cmd, shell)
94 case('wait')
95 call builtin_wait(cmd, shell)
96 case('trap')
97 call builtin_trap(cmd, shell)
98 case('config')
99 call builtin_config(cmd, shell)
100 case('alias')
101 call builtin_alias(cmd, shell)
102 case('unalias')
103 call builtin_unalias(cmd, shell)
104 case('help')
105 call builtin_help(cmd, shell)
106 case('perf')
107 call builtin_perf(cmd, shell)
108 case('memory')
109 call builtin_memory(cmd, shell)
110 case('rawtest')
111 call builtin_rawtest(cmd, shell)
112 case('defun')
113 call builtin_defun(cmd, shell)
114 case('test', '[', '[[')
115 call execute_test_command(cmd, shell)
116 case('set')
117 call builtin_set(cmd, shell)
118 case('shopt')
119 call builtin_shopt(cmd, shell)
120 case('type')
121 call builtin_type(cmd, shell)
122 case('unset')
123 call builtin_unset(cmd, shell)
124 case('readonly')
125 call builtin_readonly(cmd, shell)
126 case('shift')
127 call builtin_shift(cmd, shell)
128 case('break')
129 call builtin_break(cmd, shell)
130 case('continue')
131 call builtin_continue(cmd, shell)
132 case('return')
133 call builtin_return(cmd, shell)
134 case('exec')
135 call builtin_exec(cmd, shell)
136 case('eval')
137 call builtin_eval(cmd, shell)
138 case('hash')
139 call builtin_hash(cmd, shell)
140 case('umask')
141 call builtin_umask(cmd, shell)
142 case('ulimit')
143 call builtin_ulimit(cmd, shell)
144 case('times')
145 call builtin_times(cmd, shell)
146 case default
147 ! Should not reach here if is_builtin works correctly
148 shell%last_exit_status = 1
149 end select
150 end subroutine
151
152 subroutine builtin_exit(cmd, shell)
153 type(command_t), intent(in) :: cmd
154 type(shell_state_t), intent(inout) :: shell
155
156 shell%running = .false.
157 if (cmd%num_tokens > 1) then
158 read(cmd%tokens(2), *, iostat=shell%last_exit_status) shell%last_exit_status
159 end if
160 end subroutine
161
162 subroutine builtin_cd(cmd, shell)
163 type(command_t), intent(in) :: cmd
164 type(shell_state_t), intent(inout) :: shell
165 character(len=:), allocatable :: target_dir
166
167 if (cmd%num_tokens == 1) then
168 target_dir = get_environment_var('HOME')
169 else
170 target_dir = trim(cmd%tokens(2))
171 end if
172
173 if (change_directory(target_dir)) then
174 shell%cwd = get_current_directory()
175 shell%last_exit_status = 0
176 else
177 write(error_unit, '(a)') 'cd: cannot access ' // trim(target_dir) // &
178 ': No such file or directory. Use "pwd" to see current location.'
179 shell%last_exit_status = 1
180 end if
181 end subroutine
182
183 subroutine builtin_pwd(cmd, shell)
184 type(command_t), intent(in) :: cmd
185 type(shell_state_t), intent(inout) :: shell
186
187 write(output_unit, '(a)') trim(shell%cwd)
188 shell%last_exit_status = 0
189 end subroutine
190
191 subroutine builtin_export(cmd, shell)
192 type(command_t), intent(in) :: cmd
193 type(shell_state_t), intent(inout) :: shell
194 integer :: eq_pos
195 character(len=MAX_TOKEN_LEN) :: var_name, var_value
196
197 if (cmd%num_tokens < 2) then
198 write(error_unit, '(a)') 'export: usage: export VAR=value'
199 shell%last_exit_status = 1
200 return
201 end if
202
203 eq_pos = index(cmd%tokens(2), '=')
204 if (eq_pos > 0) then
205 var_name = cmd%tokens(2)(:eq_pos-1)
206 var_value = cmd%tokens(2)(eq_pos+1:)
207
208 if (set_environment_var(trim(var_name), trim(var_value))) then
209 shell%last_exit_status = 0
210 else
211 write(error_unit, '(a)') 'export: failed to set variable'
212 shell%last_exit_status = 1
213 end if
214 else
215 write(error_unit, '(a)') 'export: usage: export VAR=value'
216 shell%last_exit_status = 1
217 end if
218 end subroutine
219
220 subroutine builtin_echo(cmd, shell)
221 type(command_t), intent(in) :: cmd
222 type(shell_state_t), intent(inout) :: shell
223 integer :: i
224 logical :: first
225
226 ! Simple echo implementation
227 if (.not. allocated(cmd%tokens) .or. cmd%num_tokens < 1) then
228 write(*,'(a)') ''
229 shell%last_exit_status = 0
230 return
231 end if
232
233 first = .true.
234 do i = 2, cmd%num_tokens
235 if (.not. first) write(*,'(a)',advance='no') ' '
236 write(*,'(a)',advance='no') trim(cmd%tokens(i))
237 first = .false.
238 end do
239 write(*,'(a)') ''
240
241 shell%last_exit_status = 0
242 end subroutine
243
244 subroutine builtin_jobs(cmd, shell)
245 type(command_t), intent(in) :: cmd
246 type(shell_state_t), intent(inout) :: shell
247 logical :: show_pids
248
249 ! Check for -p flag to show PIDs
250 show_pids = .false.
251 if (cmd%num_tokens > 1 .and. trim(cmd%tokens(2)) == '-p') then
252 show_pids = .true.
253 end if
254
255 call list_jobs(shell, show_pids)
256 shell%last_exit_status = 0
257 end subroutine
258
259 subroutine builtin_fg(cmd, shell)
260 type(command_t), intent(in) :: cmd
261 type(shell_state_t), intent(inout) :: shell
262 integer :: job_id, iostat, i
263
264 if (cmd%num_tokens < 2) then
265 ! Bring most recent stopped job to foreground
266 job_id = 0
267 do i = MAX_JOBS, 1, -1
268 if (shell%jobs(i)%job_id > 0 .and. shell%jobs(i)%state == JOB_STOPPED) then
269 job_id = shell%jobs(i)%job_id
270 exit
271 end if
272 end do
273
274 if (job_id == 0) then
275 write(error_unit, '(a)') 'fg: no stopped job'
276 shell%last_exit_status = 1
277 return
278 end if
279 else
280 ! Parse job number (handle %n syntax)
281 if (cmd%tokens(2)(1:1) == '%') then
282 read(cmd%tokens(2)(2:), *, iostat=iostat) job_id
283 else
284 read(cmd%tokens(2), *, iostat=iostat) job_id
285 end if
286
287 if (iostat /= 0) then
288 write(error_unit, '(a)') 'fg: invalid job id'
289 shell%last_exit_status = 1
290 return
291 end if
292 end if
293
294 call resume_job_fg(shell, job_id)
295 end subroutine
296
297 subroutine builtin_bg(cmd, shell)
298 type(command_t), intent(in) :: cmd
299 type(shell_state_t), intent(inout) :: shell
300 integer :: job_id, iostat, i
301
302 if (cmd%num_tokens < 2) then
303 ! Continue most recent stopped job in background
304 job_id = 0
305 do i = MAX_JOBS, 1, -1
306 if (shell%jobs(i)%job_id > 0 .and. &
307 shell%jobs(i)%state == JOB_STOPPED) then
308 job_id = shell%jobs(i)%job_id
309 exit
310 end if
311 end do
312
313 if (job_id == 0) then
314 write(error_unit, '(a)') 'bg: no stopped job'
315 shell%last_exit_status = 1
316 return
317 end if
318 else
319 ! Parse job number (handle %n syntax)
320 if (cmd%tokens(2)(1:1) == '%') then
321 read(cmd%tokens(2)(2:), *, iostat=iostat) job_id
322 else
323 read(cmd%tokens(2), *, iostat=iostat) job_id
324 end if
325
326 if (iostat /= 0) then
327 write(error_unit, '(a)') 'bg: invalid job id'
328 shell%last_exit_status = 1
329 return
330 end if
331 end if
332
333 call resume_job_bg(shell, job_id)
334 end subroutine
335
336 subroutine builtin_source(cmd, shell)
337 type(command_t), intent(in) :: cmd
338 type(shell_state_t), intent(inout) :: shell
339
340 character(len=1024) :: filename
341 logical :: file_exists
342
343 ! Check if filename provided
344 if (cmd%num_tokens < 2) then
345 write(error_unit, '(a)') 'source: usage: source filename [arguments...]'
346 shell%last_exit_status = 1
347 return
348 end if
349
350 filename = trim(cmd%tokens(2))
351
352 ! Check if file exists and is readable
353 inquire(file=filename, exist=file_exists)
354 if (.not. file_exists) then
355 write(error_unit, '(a)') 'source: ' // trim(filename) // ': No such file or directory'
356 shell%last_exit_status = 1
357 return
358 end if
359
360 ! Mark the shell to source this file on next main loop iteration
361 ! This avoids circular dependency issues
362 shell%source_file = filename
363 shell%should_source = .true.
364 shell%last_exit_status = 0
365
366 write(output_unit, '(a)') 'source: ' // trim(filename) // ' queued for execution'
367 end subroutine
368
369 subroutine builtin_history(cmd, shell)
370 type(command_t), intent(in) :: cmd
371 type(shell_state_t), intent(inout) :: shell
372
373 ! Handle history command options
374 if (cmd%num_tokens > 1) then
375 select case(trim(cmd%tokens(2)))
376 case('-c', '--clear')
377 call clear_history()
378 write(output_unit, '(a)') 'Command history cleared.'
379 case default
380 write(error_unit, '(a)') 'history: unknown option'
381 shell%last_exit_status = 1
382 return
383 end select
384 else
385 ! Show all history
386 call show_history()
387 end if
388
389 shell%last_exit_status = 0
390 end subroutine
391
392 subroutine builtin_kill(cmd, shell)
393 type(command_t), intent(in) :: cmd
394 type(shell_state_t), intent(inout) :: shell
395 integer :: signal_num, target_pid, iostat, ret
396 integer :: i, arg_start
397 logical :: found_signal
398
399 signal_num = 15 ! Default: SIGTERM
400 arg_start = 2
401 found_signal = .false.
402
403 if (cmd%num_tokens < 2) then
404 write(error_unit, '(a)') 'kill: usage: kill [-signal] pid...'
405 shell%last_exit_status = 1
406 return
407 end if
408
409 ! Check if first argument is a signal specifier or -l flag
410 if (cmd%tokens(2)(1:1) == '-') then
411 if (len_trim(cmd%tokens(2)) > 1) then
412 ! Check for -l flag (list signals)
413 if (trim(cmd%tokens(2)) == '-l') then
414 write(output_unit, '(a)') 'Available signals:'
415 write(output_unit, '(a)') ' 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL'
416 write(output_unit, '(a)') ' 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE'
417 write(output_unit, '(a)') ' 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2'
418 write(output_unit, '(a)') ' 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT'
419 write(output_unit, '(a)') ' 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP'
420 write(output_unit, '(a)') ' 21) SIGTTIN 22) SIGTTOU'
421 shell%last_exit_status = 0
422 return
423 end if
424
425 read(cmd%tokens(2)(2:), *, iostat=iostat) signal_num
426 if (iostat /= 0) then
427 ! Try named signals
428 select case(trim(cmd%tokens(2)(2:)))
429 case('TERM', 'term')
430 signal_num = 15
431 case('KILL', 'kill')
432 signal_num = 9
433 case('INT', 'int')
434 signal_num = 2
435 case('STOP', 'stop')
436 signal_num = 19
437 case('CONT', 'cont')
438 signal_num = 18
439 case('HUP', 'hup')
440 signal_num = 1
441 case('QUIT', 'quit')
442 signal_num = 3
443 case default
444 write(error_unit, '(a)') 'kill: invalid signal specification'
445 shell%last_exit_status = 1
446 return
447 end select
448 end if
449 found_signal = .true.
450 arg_start = 3
451 end if
452 end if
453
454 if (cmd%num_tokens < arg_start) then
455 write(error_unit, '(a)') 'kill: usage: kill [-signal] pid...'
456 shell%last_exit_status = 1
457 return
458 end if
459
460 ! Kill each specified process
461 do i = arg_start, cmd%num_tokens
462 ! Handle job syntax (%n)
463 if (cmd%tokens(i)(1:1) == '%') then
464 read(cmd%tokens(i)(2:), *, iostat=iostat) target_pid
465 if (iostat == 0) then
466 ! Find job by job_id and get its pgid
467 target_pid = find_job_pgid(shell, target_pid)
468 if (target_pid <= 0) then
469 write(error_unit, '(a)') 'kill: no such job'
470 shell%last_exit_status = 1
471 cycle
472 end if
473 target_pid = -target_pid ! Kill entire process group
474 else
475 write(error_unit, '(a)') 'kill: invalid job specification'
476 shell%last_exit_status = 1
477 cycle
478 end if
479 else
480 read(cmd%tokens(i), *, iostat=iostat) target_pid
481 if (iostat /= 0) then
482 write(error_unit, '(a)') 'kill: invalid pid'
483 shell%last_exit_status = 1
484 cycle
485 end if
486 end if
487
488 ret = c_kill(int(target_pid, c_pid_t), int(signal_num, c_int))
489 if (ret /= 0) then
490 write(error_unit, '(a,i0)') 'kill: failed to kill process ', target_pid
491 shell%last_exit_status = 1
492 end if
493 end do
494
495 if (shell%last_exit_status /= 1) then
496 shell%last_exit_status = 0
497 end if
498 end subroutine
499
500
501 subroutine builtin_wait(cmd, shell)
502 type(command_t), intent(in) :: cmd
503 type(shell_state_t), intent(inout) :: shell
504 integer :: target_pid, iostat, ret
505 integer(c_int), target :: wait_status
506 integer :: i
507
508 if (cmd%num_tokens == 1) then
509 ! Wait for all background jobs
510 do i = 1, MAX_JOBS
511 if (shell%jobs(i)%job_id > 0 .and. &
512 shell%jobs(i)%state == JOB_RUNNING) then
513 ret = c_waitpid(shell%jobs(i)%pgid, c_loc(wait_status), 0)
514 if (WIFEXITED(wait_status)) then
515 shell%jobs(i)%state = JOB_DONE
516 end if
517 end if
518 end do
519 else
520 ! Wait for specific job or PID
521 do i = 2, cmd%num_tokens
522 if (cmd%tokens(i)(1:1) == '%') then
523 ! Job syntax
524 read(cmd%tokens(i)(2:), *, iostat=iostat) target_pid
525 if (iostat == 0) then
526 target_pid = find_job_pgid(shell, target_pid)
527 else
528 write(error_unit, '(a)') 'wait: invalid job specification'
529 shell%last_exit_status = 1
530 cycle
531 end if
532 else
533 read(cmd%tokens(i), *, iostat=iostat) target_pid
534 if (iostat /= 0) then
535 write(error_unit, '(a)') 'wait: invalid pid'
536 shell%last_exit_status = 1
537 cycle
538 end if
539 end if
540
541 if (target_pid > 0) then
542 ret = c_waitpid(int(target_pid, c_pid_t), c_loc(wait_status), 0)
543 if (ret > 0) then
544 if (WIFEXITED(wait_status)) then
545 shell%last_exit_status = WEXITSTATUS(wait_status)
546 else
547 shell%last_exit_status = 1
548 end if
549 else
550 write(error_unit, '(a,i0)') 'wait: pid ', target_pid, ' is not a child of this shell'
551 shell%last_exit_status = 1
552 end if
553 end if
554 end do
555 end if
556
557 if (shell%last_exit_status /= 1) then
558 shell%last_exit_status = 0
559 end if
560 end subroutine
561
562 subroutine builtin_trap(cmd, shell)
563 type(command_t), intent(in) :: cmd
564 type(shell_state_t), intent(inout) :: shell
565
566 ! Simplified trap implementation - in a full shell this would
567 ! handle signal trap management
568 if (cmd%num_tokens < 3) then
569 write(error_unit, '(a)') 'trap: usage: trap action signal...'
570 shell%last_exit_status = 1
571 return
572 end if
573
574 ! For now, just acknowledge the trap command
575 write(output_unit, '(a)') 'trap: signal trapping not yet fully implemented'
576 shell%last_exit_status = 0
577 end subroutine
578
579 subroutine builtin_config(cmd, shell)
580 type(command_t), intent(in) :: cmd
581 type(shell_state_t), intent(inout) :: shell
582
583 if (cmd%num_tokens == 1) then
584 ! Show current config
585 call show_config()
586 else
587 select case(trim(cmd%tokens(2)))
588 case('show')
589 call show_config()
590 case('create')
591 call create_default_config()
592 case('reload')
593 call load_config_file(shell)
594 case default
595 write(error_unit, '(a)') 'config: usage: config [show|create|reload]'
596 shell%last_exit_status = 1
597 return
598 end select
599 end if
600
601 shell%last_exit_status = 0
602 end subroutine
603
604 subroutine builtin_alias(cmd, shell)
605 type(command_t), intent(in) :: cmd
606 type(shell_state_t), intent(inout) :: shell
607 integer :: eq_pos
608 character(len=256) :: alias_name, alias_command
609
610 if (cmd%num_tokens == 1) then
611 ! Show all aliases
612 call show_aliases(shell)
613 else if (cmd%num_tokens == 2) then
614 ! Check for alias=command format
615 eq_pos = index(cmd%tokens(2), '=')
616 if (eq_pos > 0) then
617 alias_name = cmd%tokens(2)(:eq_pos-1)
618 alias_command = cmd%tokens(2)(eq_pos+1:)
619
620 ! Remove quotes if present
621 if (alias_command(1:1) == '"' .and. alias_command(len_trim(alias_command):len_trim(alias_command)) == '"') then
622 alias_command = alias_command(2:len_trim(alias_command)-1)
623 else if (alias_command(1:1) == "'" .and. alias_command(len_trim(alias_command):len_trim(alias_command)) == "'") then
624 alias_command = alias_command(2:len_trim(alias_command)-1)
625 end if
626
627 call set_alias(shell, trim(alias_name), trim(alias_command))
628 else
629 ! Show specific alias
630 alias_name = cmd%tokens(2)
631 alias_command = get_alias(shell, trim(alias_name))
632 if (len(alias_command) > 0) then
633 write(output_unit, '(a)') 'alias ' // trim(alias_name) // &
634 '=' // "'" // trim(alias_command) // "'"
635 else
636 write(error_unit, '(a)') 'alias: ' // trim(alias_name) // ': not found'
637 shell%last_exit_status = 1
638 return
639 end if
640 end if
641 else
642 write(error_unit, '(a)') 'alias: usage: alias [name[=value]...]'
643 shell%last_exit_status = 1
644 return
645 end if
646
647 shell%last_exit_status = 0
648 end subroutine
649
650 subroutine builtin_unalias(cmd, shell)
651 type(command_t), intent(in) :: cmd
652 type(shell_state_t), intent(inout) :: shell
653 integer :: i
654
655 if (cmd%num_tokens < 2) then
656 write(error_unit, '(a)') 'unalias: usage: unalias name...'
657 shell%last_exit_status = 1
658 return
659 end if
660
661 ! Remove each specified alias
662 do i = 2, cmd%num_tokens
663 call unset_alias(shell, trim(cmd%tokens(i)))
664 end do
665
666 shell%last_exit_status = 0
667 end subroutine
668
669 subroutine builtin_help(cmd, shell)
670 type(command_t), intent(in) :: cmd
671 type(shell_state_t), intent(inout) :: shell
672
673 write(output_unit, '(a)') 'Fortran Shell (fortsh) - Built-in Commands:'
674 write(output_unit, '(a)') '========================================'
675 write(output_unit, '(a)') ''
676 write(output_unit, '(a)') 'Navigation & Files:'
677 write(output_unit, '(a)') ' cd [dir] - Change directory (use cd ~ for home)'
678 write(output_unit, '(a)') ' pwd - Print working directory'
679 write(output_unit, '(a)') ''
680 write(output_unit, '(a)') 'Variables & Environment:'
681 write(output_unit, '(a)') ' export VAR=val - Set environment variable'
682 write(output_unit, '(a)') ' echo [args] - Display text'
683 write(output_unit, '(a)') ''
684 write(output_unit, '(a)') 'Job Control:'
685 write(output_unit, '(a)') ' jobs - List active jobs'
686 write(output_unit, '(a)') ' fg [%n] - Bring job to foreground'
687 write(output_unit, '(a)') ' bg [%n] - Send job to background'
688 write(output_unit, '(a)') ' kill [-sig] pid - Send signal to process'
689 write(output_unit, '(a)') ' wait [pid] - Wait for process to complete'
690 write(output_unit, '(a)') ''
691 write(output_unit, '(a)') 'Shell Features:'
692 write(output_unit, '(a)') ' history - Show command history'
693 write(output_unit, '(a)') ' alias [n=cmd] - Create/show command aliases'
694 write(output_unit, '(a)') ' unalias name - Remove alias'
695 write(output_unit, '(a)') ' config [cmd] - Manage shell configuration (.fshrc)'
696 write(output_unit, '(a)') ''
697 write(output_unit, '(a)') 'Control Flow:'
698 write(output_unit, '(a)') ' test / [ ] - Evaluate conditions'
699 write(output_unit, '(a)') ' if/then/else/fi - Conditional execution'
700 write(output_unit, '(a)') ' while/do/done - Loop constructs'
701 write(output_unit, '(a)') ''
702 write(output_unit, '(a)') 'Other:'
703 write(output_unit, '(a)') ' source file - Execute script (not yet implemented)'
704 write(output_unit, '(a)') ' trap - Signal handling (basic support)'
705 write(output_unit, '(a)') ' rawtest - Test raw terminal input (interactive only)'
706 write(output_unit, '(a)') ' help - Show this help message'
707 write(output_unit, '(a)') ' exit [code] - Exit shell'
708 write(output_unit, '(a)') ''
709 write(output_unit, '(a)') 'Interactive Editing (available in interactive mode):'
710 write(output_unit, '(a)') ' ↑/↓ - Navigate command history'
711 write(output_unit, '(a)') ' ←/→, Ctrl+B/F - Move cursor left/right'
712 write(output_unit, '(a)') ' Ctrl+A - Move to beginning of line (Home)'
713 write(output_unit, '(a)') ' Ctrl+E - Move to end of line (End)'
714 write(output_unit, '(a)') ' Tab - Smart command/file completion'
715 write(output_unit, '(a)') ' Ctrl+K - Kill text to end of line'
716 write(output_unit, '(a)') ' Ctrl+U - Kill entire line'
717 write(output_unit, '(a)') ' Ctrl+W - Kill previous word'
718 write(output_unit, '(a)') ' Ctrl+Y - Yank (paste) killed text'
719 write(output_unit, '(a)') ' Ctrl+L - Clear screen'
720 write(output_unit, '(a)') ''
721 write(output_unit, '(a)') 'Features: Advanced readline, tab completion, history, aliases, job control'
722
723 shell%last_exit_status = 0
724 end subroutine
725
726 subroutine builtin_perf(cmd, shell)
727 type(command_t), intent(in) :: cmd
728 type(shell_state_t), intent(inout) :: shell
729
730 if (cmd%num_tokens > 1) then
731 select case(trim(cmd%tokens(2)))
732 case('on')
733 call set_performance_monitoring(.true.)
734 write(output_unit, '(a)') 'Performance monitoring enabled'
735 case('off')
736 call set_performance_monitoring(.false.)
737 write(output_unit, '(a)') 'Performance monitoring disabled'
738 case('stats', 'status')
739 call print_performance_stats()
740 case('reset')
741 total_commands = 0
742 total_parse_time = 0
743 total_exec_time = 0
744 total_glob_time = 0
745 write(output_unit, '(a)') 'Performance counters reset'
746 case default
747 write(error_unit, '(a)') 'perf: Usage: perf [on|off|stats|reset]'
748 shell%last_exit_status = 1
749 return
750 end select
751 else
752 ! Show current status
753 if (perf_monitoring_enabled) then
754 write(output_unit, '(a)') 'Performance monitoring: ENABLED'
755 else
756 write(output_unit, '(a)') 'Performance monitoring: DISABLED'
757 end if
758 write(output_unit, '(a,i0)') 'Commands processed: ', total_commands
759 write(output_unit, '(a,i0,a)') 'Memory usage: ', get_memory_usage(), ' KB'
760 end if
761
762 shell%last_exit_status = 0
763 end subroutine
764
765 subroutine builtin_memory(cmd, shell)
766 type(command_t), intent(in) :: cmd
767 type(shell_state_t), intent(inout) :: shell
768
769 if (cmd%num_tokens > 1) then
770 select case(trim(cmd%tokens(2)))
771 case('optimize')
772 call optimize_memory_pools()
773 write(output_unit, '(a)') 'Memory pools optimized'
774 case('stats')
775 call print_pool_stats()
776 case('auto')
777 call auto_optimize_memory()
778 write(output_unit, '(a)') 'Auto memory optimization triggered'
779 case default
780 write(error_unit, '(a)') 'memory: Usage: memory [optimize|stats|auto]'
781 shell%last_exit_status = 1
782 return
783 end select
784 else
785 ! Show memory status
786 write(output_unit, '(a)') 'Memory Usage Summary:'
787 write(output_unit, '(a)') '===================='
788 write(output_unit, '(a,i0)') 'Current allocations: ', current_allocations
789 write(output_unit, '(a,i0)') 'Peak allocations: ', peak_allocations
790 write(output_unit, '(a,i0,a)') 'Current memory: ', current_memory_used, ' bytes'
791 write(output_unit, '(a,i0,a)') 'Peak memory: ', peak_memory_used, ' bytes'
792
793 if (needs_memory_optimization()) then
794 write(output_unit, '(a)') ''
795 write(output_unit, '(a)') 'Tip: Memory optimization recommended. Run "memory optimize"'
796 end if
797 end if
798
799 shell%last_exit_status = 0
800 end subroutine
801
802 subroutine builtin_rawtest(cmd, shell)
803 type(command_t), intent(in) :: cmd
804 type(shell_state_t), intent(inout) :: shell
805 type(termios_t) :: original_termios
806 character :: ch
807 logical :: success
808 integer :: char_code
809
810 write(output_unit, '(a)') 'Raw mode test - press keys to see codes, q to quit:'
811 write(output_unit, '(a)') 'Entering raw mode...'
812
813 ! Enable raw mode
814 success = enable_raw_mode(original_termios)
815 if (.not. success) then
816 write(error_unit, '(a)') 'rawtest: Failed to enable raw mode'
817 shell%last_exit_status = 1
818 return
819 end if
820
821 ! Read characters until 'q' is pressed
822 do
823 success = read_single_char(ch)
824 if (.not. success) exit
825
826 char_code = iachar(ch)
827
828 ! Exit on 'q'
829 if (ch == 'q' .or. ch == 'Q') exit
830
831 ! Handle special characters
832 if (char_code == 27) then
833 ! Escape sequence - try to read more
834 write(output_unit, '(a)', advance='no') 'ESC '
835 success = read_single_char(ch)
836 if (success) then
837 write(output_unit, '(a,i0)', advance='no') '[', iachar(ch)
838 if (ch == '[') then
839 success = read_single_char(ch)
840 if (success) then
841 write(output_unit, '(a,i0,a)', advance='no') '[', iachar(ch), '] = '
842 select case(ch)
843 case('A')
844 write(output_unit, '(a)') 'UP ARROW'
845 case('B')
846 write(output_unit, '(a)') 'DOWN ARROW'
847 case('C')
848 write(output_unit, '(a)') 'RIGHT ARROW'
849 case('D')
850 write(output_unit, '(a)') 'LEFT ARROW'
851 case default
852 write(output_unit, '(a)') 'UNKNOWN ESCAPE'
853 end select
854 end if
855 else
856 write(output_unit, '(a)') '] = ALT+key'
857 end if
858 end if
859 else if (char_code < 32) then
860 ! Control character
861 write(output_unit, '(a,i0,a)') 'CTRL+', char_code, ' (^', char(char_code + 64), ')'
862 else if (char_code == 127) then
863 write(output_unit, '(a)') 'BACKSPACE/DELETE (127)'
864 else
865 ! Regular character
866 write(output_unit, '(a,a,a,i0,a)') 'Regular: ''', ch, ''' (', char_code, ')'
867 end if
868 end do
869
870 ! Restore terminal
871 success = restore_terminal(original_termios)
872 if (.not. success) then
873 write(error_unit, '(a)') 'rawtest: Warning - failed to restore terminal'
874 end if
875
876 write(output_unit, '(a)') ''
877 write(output_unit, '(a)') 'Raw mode test completed.'
878 shell%last_exit_status = 0
879 end subroutine
880
881 subroutine builtin_defun(cmd, shell)
882 type(command_t), intent(in) :: cmd
883 type(shell_state_t), intent(inout) :: shell
884
885 character(len=1024) :: function_body(1)
886 character(len=256) :: func_name
887
888 if (cmd%num_tokens < 3) then
889 write(error_unit, '(a)') 'defun: usage: defun function_name "command1; command2"'
890 shell%last_exit_status = 1
891 return
892 end if
893
894 func_name = trim(cmd%tokens(2))
895 function_body(1) = trim(cmd%tokens(3))
896
897 call add_function(shell, func_name, function_body, 1)
898 write(output_unit, '(a)') 'Function ' // trim(func_name) // ' defined'
899 shell%last_exit_status = 0
900 end subroutine
901
902 ! Coprocess built-in commands
903 subroutine builtin_coproc(cmd, shell)
904 type(command_t), intent(in) :: cmd
905 type(shell_state_t), intent(inout) :: shell
906
907 character(len=256) :: coproc_name
908 character(len=1024) :: command
909 integer :: coproc_id
910
911 if (cmd%num_tokens < 2) then
912 call list_coprocesses()
913 shell%last_exit_status = 0
914 return
915 end if
916
917 if (cmd%num_tokens == 2) then
918 ! coproc command
919 command = trim(cmd%tokens(2))
920 coproc_id = start_coprocess(command)
921 else
922 ! coproc name command
923 coproc_name = trim(cmd%tokens(2))
924 command = trim(cmd%tokens(3))
925 coproc_id = start_coprocess(command, coproc_name)
926 end if
927
928 if (coproc_id > 0) then
929 shell%last_exit_status = 0
930 else
931 shell%last_exit_status = 1
932 end if
933 end subroutine
934
935 subroutine builtin_timeout(cmd, shell)
936 type(command_t), intent(in) :: cmd
937 type(shell_state_t), intent(inout) :: shell
938
939 integer :: timeout_seconds, i
940 character(len=1024) :: command
941
942 if (cmd%num_tokens < 3) then
943 write(error_unit, '(a)') 'timeout: usage: timeout DURATION COMMAND...'
944 shell%last_exit_status = 1
945 return
946 end if
947
948 read(cmd%tokens(2), *, iostat=i) timeout_seconds
949 if (i /= 0 .or. timeout_seconds <= 0) then
950 write(error_unit, '(a)') 'timeout: invalid duration'
951 shell%last_exit_status = 1
952 return
953 end if
954
955 ! Reconstruct command from remaining tokens
956 command = ''
957 do i = 3, cmd%num_tokens
958 if (i > 3) command = trim(command) // ' '
959 command = trim(command) // trim(cmd%tokens(i))
960 end do
961
962 ! Execute command with timeout - placeholder
963 shell%last_exit_status = 0
964 end subroutine
965
966 ! =============================================================================
967 ! POSIX Required Built-ins (Phase 10: Critical POSIX Compliance)
968 ! =============================================================================
969
970 subroutine builtin_type(cmd, shell)
971 type(command_t), intent(in) :: cmd
972 type(shell_state_t), intent(inout) :: shell
973
974 character(len=256) :: command_name
975 integer :: i
976
977 if (cmd%num_tokens < 2) then
978 write(error_unit, '(a)') 'type: usage: type name [name ...]'
979 shell%last_exit_status = 1
980 return
981 end if
982
983 do i = 2, cmd%num_tokens
984 command_name = trim(cmd%tokens(i))
985
986 if (is_builtin(command_name)) then
987 write(output_unit, '(a)') trim(command_name) // ' is a shell builtin'
988 else if (is_alias(shell, command_name)) then
989 write(output_unit, '(a)') trim(command_name) // ' is aliased to `' // &
990 trim(get_alias(shell, command_name)) // "'"
991 else if (is_function(shell, command_name)) then
992 write(output_unit, '(a)') trim(command_name) // ' is a function'
993 else
994 ! Try to find in PATH
995 call find_command_in_path(shell, command_name, .false., .false.)
996 if (shell%last_exit_status == 0) then
997 write(output_unit, '(a)') trim(command_name) // ' is hashed'
998 else
999 write(output_unit, '(a)') trim(command_name) // ': not found'
1000 shell%last_exit_status = 1
1001 end if
1002 end if
1003 end do
1004
1005 shell%last_exit_status = 0
1006 end subroutine
1007
1008 subroutine builtin_unset(cmd, shell)
1009 type(command_t), intent(in) :: cmd
1010 type(shell_state_t), intent(inout) :: shell
1011
1012 logical :: unset_functions = .false.
1013 character(len=256) :: var_name
1014 integer :: i, j, start_idx
1015
1016 if (cmd%num_tokens < 2) then
1017 write(error_unit, '(a)') 'unset: usage: unset [-f] name [name ...]'
1018 shell%last_exit_status = 1
1019 return
1020 end if
1021
1022 start_idx = 2
1023 if (trim(cmd%tokens(2)) == '-f') then
1024 unset_functions = .true.
1025 start_idx = 3
1026 if (cmd%num_tokens < 3) then
1027 write(error_unit, '(a)') 'unset: usage: unset [-f] name [name ...]'
1028 shell%last_exit_status = 1
1029 return
1030 end if
1031 end if
1032
1033 do i = start_idx, cmd%num_tokens
1034 var_name = trim(cmd%tokens(i))
1035
1036 if (unset_functions) then
1037 ! Unset function
1038 do j = 1, shell%num_functions
1039 if (trim(shell%functions(j)%name) == var_name) then
1040 shell%functions(j)%name = ''
1041 shell%functions(j)%body_lines = 0
1042 if (allocated(shell%functions(j)%body)) deallocate(shell%functions(j)%body)
1043 exit
1044 end if
1045 end do
1046 else
1047 ! Unset variable
1048 do j = 1, shell%num_variables
1049 if (trim(shell%variables(j)%name) == var_name) then
1050 shell%variables(j)%name = ''
1051 shell%variables(j)%value = ''
1052 shell%variables(j)%is_array = .false.
1053 shell%variables(j)%is_assoc_array = .false.
1054 shell%variables(j)%array_size = 0
1055 shell%variables(j)%assoc_size = 0
1056 exit
1057 end if
1058 end do
1059 end if
1060 end do
1061
1062 shell%last_exit_status = 0
1063 end subroutine
1064
1065 subroutine builtin_readonly(cmd, shell)
1066 type(command_t), intent(in) :: cmd
1067 type(shell_state_t), intent(inout) :: shell
1068
1069 ! TODO: Implement proper readonly variable support
1070 ! For now, treat as regular variable assignment
1071 if (cmd%num_tokens < 2) then
1072 write(error_unit, '(a)') 'readonly: usage: readonly name[=value] ...'
1073 shell%last_exit_status = 1
1074 return
1075 end if
1076
1077 write(output_unit, '(a)') 'readonly: feature not fully implemented'
1078 shell%last_exit_status = 0
1079 end subroutine
1080
1081 subroutine builtin_shift(cmd, shell)
1082 type(command_t), intent(in) :: cmd
1083 type(shell_state_t), intent(inout) :: shell
1084 integer :: shift_count, iostat
1085
1086 shift_count = 1 ! Default shift by 1
1087
1088 if (cmd%num_tokens > 1) then
1089 ! Parse shift count from argument
1090 read(cmd%tokens(2), *, iostat=iostat) shift_count
1091 if (iostat /= 0) then
1092 write(error_unit, '(a)') 'shift: numeric argument required'
1093 shell%last_exit_status = 1
1094 return
1095 end if
1096 end if
1097
1098 if (shift_count < 0) then
1099 write(error_unit, '(a)') 'shift: shift count out of range'
1100 shell%last_exit_status = 1
1101 return
1102 end if
1103
1104 if (shift_count > shell%num_positional) then
1105 write(error_unit, '(a)') 'shift: shift count out of range'
1106 shell%last_exit_status = 1
1107 return
1108 end if
1109
1110 call shift_positional_params(shell, shift_count)
1111 shell%last_exit_status = 0
1112 end subroutine
1113
1114 subroutine builtin_break(cmd, shell)
1115 type(command_t), intent(in) :: cmd
1116 type(shell_state_t), intent(inout) :: shell
1117
1118 ! TODO: Implement proper loop breaking
1119 ! This requires control flow integration
1120 write(output_unit, '(a)') 'break: feature not fully implemented'
1121 shell%last_exit_status = 0
1122 end subroutine
1123
1124 subroutine builtin_continue(cmd, shell)
1125 type(command_t), intent(in) :: cmd
1126 type(shell_state_t), intent(inout) :: shell
1127
1128 ! TODO: Implement proper loop continuation
1129 ! This requires control flow integration
1130 write(output_unit, '(a)') 'continue: feature not fully implemented'
1131 shell%last_exit_status = 0
1132 end subroutine
1133
1134 subroutine builtin_return(cmd, shell)
1135 type(command_t), intent(in) :: cmd
1136 type(shell_state_t), intent(inout) :: shell
1137
1138 integer :: return_code = 0
1139
1140 if (cmd%num_tokens > 1) then
1141 read(cmd%tokens(2), *, iostat=return_code) return_code
1142 if (return_code /= 0) return_code = 0
1143 end if
1144
1145 ! TODO: Implement proper function return mechanism
1146 ! For now, just set exit status
1147 shell%last_exit_status = return_code
1148 end subroutine
1149
1150 subroutine builtin_exec(cmd, shell)
1151 type(command_t), intent(in) :: cmd
1152 type(shell_state_t), intent(inout) :: shell
1153
1154 ! TODO: Implement process replacement
1155 ! This is complex and requires careful implementation
1156 write(output_unit, '(a)') 'exec: feature not fully implemented'
1157 shell%last_exit_status = 0
1158 end subroutine
1159
1160 subroutine builtin_eval(cmd, shell)
1161 type(command_t), intent(in) :: cmd
1162 type(shell_state_t), intent(inout) :: shell
1163
1164 character(len=2048) :: eval_command
1165 integer :: i
1166
1167 if (cmd%num_tokens < 2) then
1168 shell%last_exit_status = 0
1169 return
1170 end if
1171
1172 ! Concatenate all arguments
1173 eval_command = trim(cmd%tokens(2))
1174 do i = 3, cmd%num_tokens
1175 eval_command = trim(eval_command) // ' ' // trim(cmd%tokens(i))
1176 end do
1177
1178 ! TODO: Implement proper command parsing and execution
1179 ! For now, just echo what would be evaluated
1180 write(output_unit, '(a)') 'eval would execute: ' // trim(eval_command)
1181 shell%last_exit_status = 0
1182 end subroutine
1183
1184 subroutine builtin_hash(cmd, shell)
1185 type(command_t), intent(in) :: cmd
1186 type(shell_state_t), intent(inout) :: shell
1187
1188 ! TODO: Implement command hashing
1189 write(output_unit, '(a)') 'hash: feature not fully implemented'
1190 shell%last_exit_status = 0
1191 end subroutine
1192
1193 subroutine builtin_umask(cmd, shell)
1194 type(command_t), intent(in) :: cmd
1195 type(shell_state_t), intent(inout) :: shell
1196
1197 ! TODO: Implement file creation mask
1198 write(output_unit, '(a)') 'umask: feature not fully implemented'
1199 shell%last_exit_status = 0
1200 end subroutine
1201
1202 subroutine builtin_ulimit(cmd, shell)
1203 type(command_t), intent(in) :: cmd
1204 type(shell_state_t), intent(inout) :: shell
1205
1206 ! TODO: Implement resource limits
1207 write(output_unit, '(a)') 'ulimit: feature not fully implemented'
1208 shell%last_exit_status = 0
1209 end subroutine
1210
1211 subroutine builtin_times(cmd, shell)
1212 type(command_t), intent(in) :: cmd
1213 type(shell_state_t), intent(inout) :: shell
1214
1215 ! TODO: Implement process time reporting
1216 write(output_unit, '(a)') 'times: feature not fully implemented'
1217 shell%last_exit_status = 0
1218 end subroutine
1219
1220 end module builtins