! ============================================================================== ! Module: command_builtin ! Purpose: Command identification built-ins (type, which, command) ! ============================================================================== module command_builtin use shell_types use variables use system_interface, only: F_OK, X_OK use iso_fortran_env, only: output_unit, error_unit use iso_c_binding, only: c_int, c_char, c_null_char use io_helpers, only: write_stdout implicit none interface function access_c(path, mode) bind(c, name='access') result(status) import :: c_int, c_char character(kind=c_char), intent(in) :: path(*) integer(c_int), value :: mode integer(c_int) :: status end function end interface contains subroutine builtin_type(cmd, shell) type(command_t), intent(in) :: cmd type(shell_state_t), intent(inout) :: shell integer :: i, arg_index logical :: all_flag, path_flag, type_flag, function_flag character(len=256) :: command_name if (cmd%num_tokens < 2) then write(error_unit, '(a)') 'type: usage: type [-afptP] name [name ...]' shell%last_exit_status = 2 return end if all_flag = .false. path_flag = .false. type_flag = .false. function_flag = .false. arg_index = 2 ! Parse options do while (arg_index <= cmd%num_tokens) if (cmd%tokens(arg_index)(1:1) == '-') then select case (trim(cmd%tokens(arg_index))) case ('-a') all_flag = .true. case ('-p') path_flag = .true. case ('-t') type_flag = .true. case ('-f') function_flag = .true. case ('-P') path_flag = .true. case ('--') arg_index = arg_index + 1 exit case default write(error_unit, '(a,a)') 'type: unknown option: ', trim(cmd%tokens(arg_index)) shell%last_exit_status = 1 return end select arg_index = arg_index + 1 else exit end if end do if (arg_index > cmd%num_tokens) then write(error_unit, '(a)') 'type: usage: type [-afptP] name [name ...]' shell%last_exit_status = 2 return end if shell%last_exit_status = 0 ! Process each command name do i = arg_index, cmd%num_tokens command_name = cmd%tokens(i) call identify_command_type(shell, command_name, all_flag, path_flag, type_flag, function_flag) end do end subroutine subroutine builtin_which(cmd, shell) type(command_t), intent(in) :: cmd type(shell_state_t), intent(inout) :: shell integer :: i, arg_index logical :: all_flag, silent_flag character(len=256) :: command_name if (cmd%num_tokens < 2) then write(error_unit, '(a)') 'which: usage: which [-as] command [command ...]' shell%last_exit_status = 2 return end if all_flag = .false. silent_flag = .false. arg_index = 2 ! Parse options do while (arg_index <= cmd%num_tokens) if (cmd%tokens(arg_index)(1:1) == '-') then select case (trim(cmd%tokens(arg_index))) case ('-a') all_flag = .true. case ('-s') silent_flag = .true. case ('--') arg_index = arg_index + 1 exit case default write(error_unit, '(a,a)') 'which: unknown option: ', trim(cmd%tokens(arg_index)) shell%last_exit_status = 1 return end select arg_index = arg_index + 1 else exit end if end do if (arg_index > cmd%num_tokens) then write(error_unit, '(a)') 'which: usage: which [-as] command [command ...]' shell%last_exit_status = 2 return end if shell%last_exit_status = 0 ! Process each command name do i = arg_index, cmd%num_tokens command_name = cmd%tokens(i) call find_command_in_path(shell, command_name, all_flag, silent_flag) end do end subroutine subroutine builtin_command(cmd, shell) use executor, only: execute_pipeline type(command_t), intent(in) :: cmd type(shell_state_t), intent(inout) :: shell integer :: arg_index, j logical :: path_flag, verbose_flag, big_v_flag character(len=256) :: command_name type(pipeline_t) :: temp_pipeline if (cmd%num_tokens < 2) then write(error_unit, '(a)') 'command: usage: command [-pVv] command [arg ...]' shell%last_exit_status = 2 return end if path_flag = .false. verbose_flag = .false. big_v_flag = .false. arg_index = 2 ! Parse options do while (arg_index <= cmd%num_tokens) if (cmd%tokens(arg_index)(1:1) == '-') then select case (trim(cmd%tokens(arg_index))) case ('-p') path_flag = .true. case ('-V') verbose_flag = .true. big_v_flag = .true. case ('-v') verbose_flag = .true. case ('--') arg_index = arg_index + 1 exit case default write(error_unit, '(a,a)') 'command: unknown option: ', trim(cmd%tokens(arg_index)) shell%last_exit_status = 1 return end select arg_index = arg_index + 1 else exit end if end do if (arg_index > cmd%num_tokens) then write(error_unit, '(a)') 'command: usage: command [-pVv] command [arg ...]' shell%last_exit_status = 2 return end if command_name = cmd%tokens(arg_index) if (verbose_flag) then if (big_v_flag) then ! command -V: verbose output like type (e.g., "echo is a shell builtin") call identify_command_type(shell, command_name, .false., path_flag, .false., .false., .true., .false.) else ! command -v: minimal output, just the path/name (POSIX format) call identify_command_type(shell, command_name, .false., path_flag, .false., .false., .true., .true.) end if ! Don't overwrite exit status set by identify_command_type else ! Execute the command bypassing functions ! Build a pipeline directly from the tokens we already have allocate(temp_pipeline%commands(1)) temp_pipeline%num_commands = 1 temp_pipeline%commands(1)%num_tokens = cmd%num_tokens - arg_index + 1 temp_pipeline%commands(1)%separator = SEP_NONE temp_pipeline%commands(1)%background = .false. temp_pipeline%commands(1)%num_redirections = 0 temp_pipeline%commands(1)%num_prefix_assignments = 0 ! Allocate and copy tokens directly from cmd allocate(character(len=MAX_TOKEN_LEN) :: temp_pipeline%commands(1)%tokens(temp_pipeline%commands(1)%num_tokens)) allocate(temp_pipeline%commands(1)%token_quoted(temp_pipeline%commands(1)%num_tokens)) allocate(temp_pipeline%commands(1)%token_escaped(temp_pipeline%commands(1)%num_tokens)) allocate(temp_pipeline%commands(1)%token_quote_type(temp_pipeline%commands(1)%num_tokens)) allocate(temp_pipeline%commands(1)%token_lengths(temp_pipeline%commands(1)%num_tokens)) do j = 1, temp_pipeline%commands(1)%num_tokens temp_pipeline%commands(1)%tokens(j) = cmd%tokens(arg_index + j - 1) if (allocated(cmd%token_quoted)) then temp_pipeline%commands(1)%token_quoted(j) = cmd%token_quoted(arg_index + j - 1) else temp_pipeline%commands(1)%token_quoted(j) = .false. end if if (allocated(cmd%token_escaped)) then temp_pipeline%commands(1)%token_escaped(j) = cmd%token_escaped(arg_index + j - 1) else temp_pipeline%commands(1)%token_escaped(j) = .false. end if if (allocated(cmd%token_quote_type)) then temp_pipeline%commands(1)%token_quote_type(j) = cmd%token_quote_type(arg_index + j - 1) else temp_pipeline%commands(1)%token_quote_type(j) = QUOTE_NONE end if if (allocated(cmd%token_lengths)) then temp_pipeline%commands(1)%token_lengths(j) = cmd%token_lengths(arg_index + j - 1) else temp_pipeline%commands(1)%token_lengths(j) = len_trim(cmd%tokens(arg_index + j - 1)) end if end do ! Set bypass flags and execute shell%bypass_functions = .true. shell%bypass_aliases = .true. call execute_pipeline(temp_pipeline, shell, '') shell%bypass_functions = .false. shell%bypass_aliases = .false. ! Cleanup if (allocated(temp_pipeline%commands(1)%tokens)) deallocate(temp_pipeline%commands(1)%tokens) if (allocated(temp_pipeline%commands(1)%token_quoted)) deallocate(temp_pipeline%commands(1)%token_quoted) if (allocated(temp_pipeline%commands(1)%token_escaped)) deallocate(temp_pipeline%commands(1)%token_escaped) if (allocated(temp_pipeline%commands(1)%token_quote_type)) deallocate(temp_pipeline%commands(1)%token_quote_type) if (allocated(temp_pipeline%commands(1)%token_lengths)) deallocate(temp_pipeline%commands(1)%token_lengths) deallocate(temp_pipeline%commands) end if end subroutine subroutine identify_command_type(shell, command_name, all_flag, path_flag, type_flag, function_flag, silent_errors, v_flag) type(shell_state_t), intent(inout) :: shell character(len=*), intent(in) :: command_name logical, intent(in) :: all_flag, path_flag, type_flag, function_flag logical, intent(in), optional :: silent_errors, v_flag logical :: found_any, suppress_errors, is_v_flag character(len=MAX_PATH_LEN) :: full_path if (.false.) print *, function_flag ! Silence unused warning found_any = .false. suppress_errors = .false. is_v_flag = .false. if (present(silent_errors)) suppress_errors = silent_errors if (present(v_flag)) is_v_flag = v_flag ! Check if it's a shell keyword if (.not. path_flag .and. is_shell_keyword(command_name)) then if (type_flag) then call write_stdout('keyword') else if (is_v_flag) then call write_stdout(trim(command_name)) else call write_stdout(trim(command_name) // ' is a shell keyword') end if found_any = .true. if (.not. all_flag) return end if ! Check if it's an alias (bash only reports aliases in interactive mode) if (.not. path_flag .and. shell%is_interactive .and. & is_shell_alias(shell, command_name)) then if (type_flag) then call write_stdout('alias') else if (is_v_flag) then call write_stdout(trim(command_name)) else call write_stdout(trim(command_name) // ' is aliased') end if found_any = .true. if (.not. all_flag) return end if ! Check if it's a function if (.not. path_flag .and. is_shell_function(shell, command_name)) then if (type_flag) then call write_stdout('function') else if (is_v_flag) then call write_stdout(trim(command_name)) else call write_stdout(trim(command_name) // ' is a function') end if found_any = .true. if (.not. all_flag) return end if ! Check if it's a built-in if (.not. path_flag .and. is_builtin_command(command_name)) then if (type_flag) then call write_stdout('builtin') else if (is_v_flag) then call write_stdout(trim(command_name)) else call write_stdout(trim(command_name) // ' is a shell builtin') end if found_any = .true. if (.not. all_flag) return end if ! Search in PATH if (find_executable_in_path(shell, command_name, full_path)) then if (type_flag) then call write_stdout('file') else if (is_v_flag) then call write_stdout(trim(full_path)) else call write_stdout(trim(command_name) // ' is ' // trim(full_path)) end if found_any = .true. end if if (.not. found_any) then if (.not. suppress_errors .and. .not. type_flag) then write(error_unit, '(a,a,a)') trim(command_name), ': not found' end if shell%last_exit_status = 1 end if end subroutine subroutine find_command_in_path(shell, command_name, all_flag, silent_flag) type(shell_state_t), intent(inout) :: shell character(len=*), intent(in) :: command_name logical, intent(in) :: all_flag, silent_flag character(len=MAX_PATH_LEN) :: full_path if (.false.) print *, all_flag ! Silence unused warning if (find_executable_in_path(shell, command_name, full_path)) then if (.not. silent_flag) then call write_stdout(trim(full_path)) end if else if (.not. silent_flag) then write(error_unit, '(a,a,a)') trim(command_name), ': not found' end if shell%last_exit_status = 1 end if end subroutine function find_executable_in_path(shell, command_name, full_path) result(found) type(shell_state_t), intent(in) :: shell character(len=*), intent(in) :: command_name character(len=*), intent(out) :: full_path logical :: found character(len=4096) :: path_var character(len=:), allocatable :: path_component character(len=MAX_PATH_LEN) :: candidate_buf character(len=:), allocatable :: candidate_path integer :: start_pos, end_pos, colon_pos found = .false. full_path = '' ! If command contains '/', it's an absolute or relative path if (index(command_name, '/') > 0) then if (is_executable_file(command_name)) then full_path = command_name found = .true. end if return end if ! Get PATH variable path_var = get_shell_variable(shell, 'PATH') if (len_trim(path_var) == 0) then path_var = '/usr/bin:/bin' end if ! Search each directory in PATH start_pos = 1 do while (start_pos <= len_trim(path_var)) colon_pos = index(path_var(start_pos:), ':') if (colon_pos == 0) then end_pos = len_trim(path_var) else end_pos = start_pos + colon_pos - 2 end if path_component = path_var(start_pos:end_pos) if (len_trim(path_component) == 0) then path_component = '.' end if ! Construct full path if (path_component(len_trim(path_component):len_trim(path_component)) == '/') then write(candidate_buf, '(a,a)') trim(path_component), trim(command_name) else write(candidate_buf, '(a,a,a)') trim(path_component), '/', trim(command_name) end if candidate_path = trim(candidate_buf) if (is_executable_file(candidate_path)) then full_path = trim(candidate_path) found = .true. return end if if (colon_pos == 0) exit start_pos = start_pos + colon_pos end do end function function is_executable_file(path) result(executable) character(len=*), intent(in) :: path logical :: executable character(kind=c_char) :: c_path(len_trim(path) + 1) integer :: i, status ! Convert to C string do i = 1, len_trim(path) c_path(i) = path(i:i) end do c_path(len_trim(path) + 1) = c_null_char ! Check if file exists and is executable status = access_c(c_path, F_OK + X_OK) executable = (status == 0) end function function is_shell_keyword(command_name) result(is_keyword) character(len=*), intent(in) :: command_name logical :: is_keyword character(len=16), parameter :: keywords(20) = [ & 'if ', 'then ', 'else ', 'elif ', 'fi ', & 'for ', 'while ', 'until ', 'do ', 'done ', & 'case ', 'esac ', 'function ', 'select ', 'time ', & 'coproc ', '{ ', '} ', '! ', '[[ ' ] integer :: i is_keyword = .false. do i = 1, size(keywords) if (trim(command_name) == trim(keywords(i))) then is_keyword = .true. return end if end do end function function is_builtin_command(command_name) result(is_builtin) character(len=*), intent(in) :: command_name logical :: is_builtin character(len=16), parameter :: builtins(56) = [ & 'cd ', 'pwd ', 'echo ', 'printf ', & 'read ', 'export ', 'unset ', 'set ', & 'shift ', 'test ', 'true ', 'false ', & 'exit ', 'return ', 'break ', 'continue ', & 'source ', '. ', 'eval ', 'exec ', & 'jobs ', 'fg ', 'bg ', 'kill ', & 'wait ', 'declare ', 'local ', 'readonly ', & 'alias ', 'unalias ', 'type ', 'command ', & 'hash ', 'trap ', 'umask ', 'ulimit ', & 'times ', 'let ', 'getopts ', 'fc ', & 'help ', 'defun ', 'abbr ', 'which ', & 'history ', 'shopt ', 'complete ', 'compgen ', & 'coproc ', 'printenv ', 'pushd ', 'popd ', & 'dirs ', 'prevd ', 'nextd ', 'dirh ' ] integer :: i is_builtin = .false. do i = 1, size(builtins) if (trim(command_name) == trim(builtins(i))) then is_builtin = .true. return end if end do end function function is_shell_function(shell, command_name) result(is_function) use ast_executor, only: is_ast_function type(shell_state_t), intent(in) :: shell character(len=*), intent(in) :: command_name logical :: is_function integer :: i is_function = .false. ! Check old executor's function storage do i = 1, shell%num_functions if (trim(shell%functions(i)%name) == trim(command_name) .and. & len_trim(shell%functions(i)%name) > 0) then is_function = .true. return end if end do ! Also check AST executor's function cache is_function = is_ast_function(command_name) end function function is_shell_alias(shell, command_name) result(is_alias) type(shell_state_t), intent(in) :: shell character(len=*), intent(in) :: command_name logical :: is_alias integer :: i is_alias = .false. do i = 1, shell%num_aliases if (trim(shell%aliases(i)%name) == trim(command_name)) then is_alias = .true. return end if end do end function function find_command_full_path(command_name) result(full_path) use system_interface, only: get_environment_var character(len=*), intent(in) :: command_name character(len=MAX_PATH_LEN) :: full_path character(len=:), allocatable :: path_var_alloc character(len=4096) :: path_var character(len=:), allocatable :: path_component character(len=MAX_PATH_LEN) :: candidate_buf character(len=:), allocatable :: candidate_path integer :: start_pos, end_pos, colon_pos full_path = '' ! If command contains '/', it's an absolute or relative path if (index(command_name, '/') > 0) then if (is_executable_file(command_name)) then full_path = command_name end if return end if ! Get PATH environment variable path_var_alloc = get_environment_var('PATH') if (allocated(path_var_alloc)) then path_var = path_var_alloc else path_var = '/usr/bin:/bin' end if if (len_trim(path_var) == 0) then path_var = '/usr/bin:/bin' end if ! Search each directory in PATH start_pos = 1 do while (start_pos <= len_trim(path_var)) colon_pos = index(path_var(start_pos:), ':') if (colon_pos == 0) then end_pos = len_trim(path_var) else end_pos = start_pos + colon_pos - 2 end if path_component = path_var(start_pos:end_pos) if (len_trim(path_component) == 0) then path_component = '.' end if ! Construct full path if (path_component(len_trim(path_component):len_trim(path_component)) == '/') then write(candidate_buf, '(a,a)') trim(path_component), trim(command_name) else write(candidate_buf, '(a,a,a)') trim(path_component), '/', trim(command_name) end if candidate_path = trim(candidate_buf) if (is_executable_file(candidate_path)) then full_path = trim(candidate_path) return end if if (colon_pos == 0) exit start_pos = start_pos + colon_pos end do end function end module command_builtin