diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index ba1a0be0..ee5a204e 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -1,10 +1,15 @@ +file(GLOB HEADERS *.h) + set(SOURCES + ${HEADERS} main.cpp - sh.h + Prompt.cpp + Command.cpp builtin/cd.cpp builtin/exit.cpp builtin/set.cpp builtin/unset.cpp + builtin/echo.cpp ) add_executable(sh ${SOURCES}) diff --git a/shell/Command.cpp b/shell/Command.cpp new file mode 100644 index 00000000..8a54c844 --- /dev/null +++ b/shell/Command.cpp @@ -0,0 +1,153 @@ +/** + * @file Command.cpp + * @author apio (cloudapio.eu) + * @brief Command parsing and execution. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include "Command.h" +#include +#include +#include +#include +#include + +extern shell_builtin_t builtin_cd; +extern shell_builtin_t builtin_exit; +extern shell_builtin_t builtin_set; +extern shell_builtin_t builtin_unset; +extern shell_builtin_t builtin_echo; + +static HashMap s_builtins; + +Result init_builtins() +{ + s_builtins = {}; + + TRY(s_builtins.try_set("cd"_sv, builtin_cd)); + TRY(s_builtins.try_set("exit"_sv, builtin_exit)); + TRY(s_builtins.try_set("set"_sv, builtin_set)); + TRY(s_builtins.try_set("unset"_sv, builtin_unset)); + TRY(s_builtins.try_set("echo"_sv, builtin_echo)); + + return {}; +} + +Result> read_command(SharedPtr file) +{ + auto maybe_cmd = file->read_line(); + if (maybe_cmd.has_error()) + { + if (maybe_cmd.error() == EINTR) + { + os::print("\n"); + return Option { String {} }; + } + return maybe_cmd.release_error(); + } + + auto cmd = maybe_cmd.release_value(); + if (cmd.is_empty()) return Option {}; + + if (strspn(cmd.chars(), " \n") == cmd.length()) return Option { String {} }; + + if (strcspn(cmd.chars(), " #") == 0) return Option { String {} }; + + return Option { move(cmd) }; +} + +Result> parse_command(StringView command) +{ + auto vec = TRY(command.split(" \n"_sv)); + + auto cmd = TRY(make_owned()); + cmd->args = move(vec); + + return cmd; +} + +shell_builtin_t* check_builtin(Command& command) +{ + if (!strchr(command.args[0].chars(), '/')) { return s_builtins.try_get_ref(command.args[0].view()); } + + return nullptr; +} + +[[noreturn]] void execute_command(OwnedPtr command) +{ + auto err = os::Process::exec(command->args[0].view(), command->args.slice()); + + os::eprintln("%s: %s", command->args[0].chars(), err.error_string()); + + os::Process::exit(127); +} + +[[noreturn]] void execute_command_or_builtin(OwnedPtr command) +{ + shell_builtin_t* builtin; + if ((builtin = check_builtin(*command))) + { + auto rc = run_builtin(command->args, *builtin); + if (rc.has_error()) os::Process::exit(rc.error()); + os::Process::exit(0); + } + + execute_command(move(command)); +} + +Result run_builtin(const Vector& args, shell_builtin_t builtin) +{ + Vector argv; + for (const auto& arg : args) { TRY(argv.try_append(arg.chars())); } + TRY(argv.try_append(nullptr)); + + auto rc = builtin((int)args.size(), const_cast(argv.data())); + if (rc.has_error()) + { + os::eprintln("%s: %s", args[0].chars(), rc.error_string()); + return rc.release_error(); + } + + return {}; +} + +Result execute_command_in_subprocess(OwnedPtr command, SharedPtr input_file, bool interactive) +{ + shell_builtin_t* builtin; + if ((builtin = check_builtin(*command))) + { + auto rc = run_builtin(command->args, *builtin); + if (rc.has_error()) return rc.error(); + return 0; + } + + pid_t child = TRY(os::Process::fork()); + + if (child == 0) + { + if (interactive) + { + setpgid(0, 0); + tcsetpgrp(input_file->fd(), getpid()); + } + + execute_command(move(command)); + } + + int status; + TRY(os::Process::wait(child, &status)); + + if (interactive) tcsetpgrp(input_file->fd(), getpgid(0)); + + if (WIFSIGNALED(status)) + { + int sig = WTERMSIG(status); + if (sig != SIGINT && sig != SIGQUIT) os::println("[sh] Process %d exited: %s", child, strsignal(sig)); + else + os::print("\n"); + } + + return status; +} diff --git a/shell/Command.h b/shell/Command.h new file mode 100644 index 00000000..85e9be68 --- /dev/null +++ b/shell/Command.h @@ -0,0 +1,36 @@ +/** + * @file Command.h + * @author apio (cloudapio.eu) + * @brief Command parsing and execution. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include "sh.h" +#include +#include +#include +#include + +struct Command +{ + Vector args; +}; + +Result init_builtins(); + +Result> read_command(SharedPtr file); + +Result> parse_command(StringView command); + +[[noreturn]] void execute_command(OwnedPtr command); + +[[noreturn]] void execute_command_or_builtin(OwnedPtr command); + +Result run_builtin(const Vector& args, shell_builtin_t builtin); + +shell_builtin_t* check_builtin(Command& command); + +Result execute_command_in_subprocess(OwnedPtr command, SharedPtr input_file, bool interactive); diff --git a/shell/Prompt.cpp b/shell/Prompt.cpp new file mode 100644 index 00000000..b43dcd4f --- /dev/null +++ b/shell/Prompt.cpp @@ -0,0 +1,46 @@ +/** + * @file Prompt.cpp + * @author apio (cloudapio.eu) + * @brief Displays the shell prompt. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include "Prompt.h" +#include +#include +#include +#include +#include +#include +#include + +struct utsname g_sysinfo; + +const char* hostname = ""; +const char* username = ""; +char prompt_end = '$'; + +void setup_prompt() +{ + // Set up everything to form a prompt. + uname(&g_sysinfo); + hostname = g_sysinfo.nodename; + + if (getuid() == 0) prompt_end = '#'; + + struct passwd* pw = getpwuid(getuid()); + if (pw) { username = pw->pw_name; } + else { username = getenv("USER"); } + endpwent(); +} + +Result display_prompt() +{ + auto cwd = TRY(os::FileSystem::working_directory()); + os::print("\x1b[%dm%s\x1b[m@\x1b[36m%s\x1b[m:\x1b[1;34m%s\x1b[m%c ", getuid() == 0 ? 31 : 35, username, hostname, + cwd.chars(), prompt_end); + + return {}; +} diff --git a/shell/Prompt.h b/shell/Prompt.h new file mode 100644 index 00000000..f7d8df3b --- /dev/null +++ b/shell/Prompt.h @@ -0,0 +1,14 @@ +/** + * @file Prompt.h + * @author apio (cloudapio.eu) + * @brief Displays the shell prompt. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include + +void setup_prompt(); +Result display_prompt(); diff --git a/shell/builtin/echo.cpp b/shell/builtin/echo.cpp new file mode 100644 index 00000000..72b6f9ae --- /dev/null +++ b/shell/builtin/echo.cpp @@ -0,0 +1,20 @@ +#include "sh.h" +#include +#include + +void print_sequenced(const char* string, bool& state) +{ + if (!state) { putchar(' '); } + state = false; + fputs(string, stdout); +} + +shell_builtin_t builtin_echo = [](int argc, char** argv) -> Result { + bool state = true; + + for (int i = 1; i < argc; i++) { print_sequenced(argv[i], state); } + + putchar('\n'); + + return {}; +}; diff --git a/shell/main.cpp b/shell/main.cpp index 80c73031..4993859c 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -1,85 +1,73 @@ +#include "Command.h" +#include "Prompt.h" #include "sh.h" #include #include #include #include +#include #include #include #include -#include #include #include #include #include #include -#include #include #include #include -extern shell_builtin_t builtin_cd; -extern shell_builtin_t builtin_exit; -extern shell_builtin_t builtin_set; -extern shell_builtin_t builtin_unset; - -static HashMap s_builtins; - using os::File; -static Result> split_command_into_args(StringView cmd) -{ - return cmd.split(" \n"_sv); -} - -static Result execute_command(StringView command) -{ - if (strcspn(command.chars(), " #") == 0) return {}; - - auto args = TRY(split_command_into_args(command)); - if (args.size() < 1) exit(0); - - return os::Process::exec(args[0].view(), args.slice()); -} - -static Result init_builtins() -{ - s_builtins = {}; - - TRY(s_builtins.try_set("cd"_sv, builtin_cd)); - TRY(s_builtins.try_set("exit"_sv, builtin_exit)); - TRY(s_builtins.try_set("set"_sv, builtin_set)); - TRY(s_builtins.try_set("unset"_sv, builtin_unset)); - - return {}; -} - -static Result builtin_wrapper(const Vector& args, shell_builtin_t builtin) -{ - Vector argv; - for (const auto& arg : args) { TRY(argv.try_append(arg.chars())); } - TRY(argv.try_append(nullptr)); - - auto rc = builtin((int)args.size(), const_cast(argv.data())); - if (rc.has_error()) - { - errno = rc.error(); - perror(argv[0]); - } - - return {}; -} - // Do nothing, but we need a handler so read() returns EINTR. static void sigint_handler(int) { } -struct utsname g_sysinfo; +static Result execute_loop(bool interactive, SharedPtr input_file) +{ + while (1) + { + if (interactive) display_prompt(); -const char* hostname = ""; -const char* username = ""; -char prompt_end = '$'; + auto maybe_cmd = TRY(read_command(input_file)); + if (!maybe_cmd.has_value()) + { + if (interactive) puts("exit"); + break; + } + + auto cmd = maybe_cmd.release_value(); + if (cmd.is_empty()) continue; + + auto command = TRY(parse_command(cmd.view())); + if (command->args.size() < 1) continue; + + TRY(execute_command_in_subprocess(move(command), input_file, interactive)); + } + + return 0; +} + +Result execute_rc_file() +{ + static const StringView rc_file_name = ".shellrc"_sv; + + auto home_path = TRY(os::FileSystem::home_directory()); + auto home_dir = TRY(os::Directory::open(home_path.view())); + + auto rc_file_path = os::Path { home_dir->fd(), rc_file_name }; + if (os::FileSystem::exists(rc_file_path)) + { + auto rc_file = TRY(os::File::open(rc_file_path, os::File::ReadOnly)); + + TRY(execute_loop(false, rc_file)); + } + + return {}; +} Result luna_main(int argc, char** argv) { @@ -97,8 +85,13 @@ Result luna_main(int argc, char** argv) parser.add_switch_argument(interactive, 'i', "interactive"_sv, "run an interactive shell"_sv); parser.parse(argc, argv); - // TODO: This does not properly handle builtins. - if (!command.is_empty()) TRY(execute_command(command)); + TRY(init_builtins()); + + if (!command.is_empty()) + { + auto cmd_obj = TRY(parse_command(command)); + execute_command_or_builtin(move(cmd_obj)); + } if (path == "-") { input_file = File::standard_input(); } else @@ -111,98 +104,15 @@ Result luna_main(int argc, char** argv) if (interactive) { - // Set up everything to form a prompt. - uname(&g_sysinfo); - hostname = g_sysinfo.nodename; - - if (getuid() == 0) prompt_end = '#'; - - struct passwd* pw = getpwuid(getuid()); - if (pw) { username = pw->pw_name; } - else { username = getenv("USER"); } - endpwent(); + setup_prompt(); signal(SIGTTOU, SIG_IGN); signal(SIGINT, sigint_handler); tcsetpgrp(input_file->fd(), getpgid(0)); + + TRY(execute_rc_file()); } - TRY(init_builtins()); - - while (1) - { - if (interactive) - { - auto cwd = TRY(os::FileSystem::working_directory()); - os::print("\x1b[%dm%s\x1b[m@\x1b[36m%s\x1b[m:\x1b[1;34m%s\x1b[m%c ", getuid() == 0 ? 31 : 35, username, - hostname, cwd.chars(), prompt_end); - } - - auto maybe_cmd = input_file->read_line(); - if (maybe_cmd.has_error()) - { - if (maybe_cmd.error() == EINTR) - { - os::print("\n"); - continue; - } - return maybe_cmd.release_error(); - } - - auto cmd = maybe_cmd.release_value(); - if (cmd.is_empty()) - { - if (interactive) puts("exit"); - break; - } - - if (strspn(cmd.chars(), " \n") == cmd.length()) continue; - - if (strcspn(cmd.chars(), " #") == 0) continue; - - auto args = TRY(split_command_into_args(cmd.view())); - if (args.size() < 1) continue; - - if (!strchr(args[0].chars(), '/')) - { - auto maybe_builtin = s_builtins.try_get(args[0].view()); - - if (maybe_builtin.has_value()) - { - auto builtin = maybe_builtin.value(); - - TRY(builtin_wrapper(args, builtin)); - - continue; - } - } - - pid_t child = TRY(os::Process::fork()); - - if (child == 0) - { - if (interactive) - { - setpgid(0, 0); - tcsetpgrp(input_file->fd(), getpid()); - } - TRY(os::Process::exec(args[0].view(), args.slice())); - } - - int status; - TRY(os::Process::wait(child, &status)); - - if (interactive) tcsetpgrp(input_file->fd(), getpgid(0)); - - if (WIFSIGNALED(status)) - { - int sig = WTERMSIG(status); - if (sig != SIGINT && sig != SIGQUIT) os::println("[sh] Process %d exited: %s", child, strsignal(sig)); - else - os::print("\n"); - } - } - - return 0; + return execute_loop(interactive, input_file); }