shell: Split code into multiple files, add the "echo" builtin, and add support for a .shellrc file
All checks were successful
Build and test / build (push) Successful in 1m46s

This commit is contained in:
apio 2024-04-20 17:17:31 +02:00
parent 646a15d295
commit 6293aeea58
Signed by: apio
GPG Key ID: B8A7D06E42258954
7 changed files with 330 additions and 146 deletions

View File

@ -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})

153
shell/Command.cpp Normal file
View File

@ -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 <luna/HashMap.h>
#include <os/Process.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
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<StringView, shell_builtin_t> s_builtins;
Result<void> 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<Option<String>> read_command(SharedPtr<os::File> file)
{
auto maybe_cmd = file->read_line();
if (maybe_cmd.has_error())
{
if (maybe_cmd.error() == EINTR)
{
os::print("\n");
return Option<String> { String {} };
}
return maybe_cmd.release_error();
}
auto cmd = maybe_cmd.release_value();
if (cmd.is_empty()) return Option<String> {};
if (strspn(cmd.chars(), " \n") == cmd.length()) return Option<String> { String {} };
if (strcspn(cmd.chars(), " #") == 0) return Option<String> { String {} };
return Option<String> { move(cmd) };
}
Result<OwnedPtr<Command>> parse_command(StringView command)
{
auto vec = TRY(command.split(" \n"_sv));
auto cmd = TRY(make_owned<Command>());
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> 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> 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<void> run_builtin(const Vector<String>& args, shell_builtin_t builtin)
{
Vector<const char*> 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<char**>(argv.data()));
if (rc.has_error())
{
os::eprintln("%s: %s", args[0].chars(), rc.error_string());
return rc.release_error();
}
return {};
}
Result<int> execute_command_in_subprocess(OwnedPtr<Command> command, SharedPtr<os::File> 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;
}

36
shell/Command.h Normal file
View File

@ -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 <luna/OwnedPtr.h>
#include <luna/String.h>
#include <luna/Vector.h>
#include <os/File.h>
struct Command
{
Vector<String> args;
};
Result<void> init_builtins();
Result<Option<String>> read_command(SharedPtr<os::File> file);
Result<OwnedPtr<Command>> parse_command(StringView command);
[[noreturn]] void execute_command(OwnedPtr<Command> command);
[[noreturn]] void execute_command_or_builtin(OwnedPtr<Command> command);
Result<void> run_builtin(const Vector<String>& args, shell_builtin_t builtin);
shell_builtin_t* check_builtin(Command& command);
Result<int> execute_command_in_subprocess(OwnedPtr<Command> command, SharedPtr<os::File> input_file, bool interactive);

46
shell/Prompt.cpp Normal file
View File

@ -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 <luna/String.h>
#include <os/File.h>
#include <os/FileSystem.h>
#include <pwd.h>
#include <stdlib.h>
#include <sys/utsname.h>
#include <unistd.h>
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<void> 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 {};
}

14
shell/Prompt.h Normal file
View File

@ -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 <luna/Result.h>
void setup_prompt();
Result<void> display_prompt();

20
shell/builtin/echo.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "sh.h"
#include <os/ArgumentParser.h>
#include <stdio.h>
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<void> {
bool state = true;
for (int i = 1; i < argc; i++) { print_sequenced(argv[i], state); }
putchar('\n');
return {};
};

View File

@ -1,85 +1,73 @@
#include "Command.h"
#include "Prompt.h"
#include "sh.h"
#include <errno.h>
#include <luna/String.h>
#include <luna/Vector.h>
#include <os/ArgumentParser.h>
#include <os/Directory.h>
#include <os/File.h>
#include <os/FileSystem.h>
#include <os/Process.h>
#include <pwd.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/utsname.h>
#include <termios.h>
#include <unistd.h>
#include <luna/HashMap.h>
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<StringView, shell_builtin_t> s_builtins;
using os::File;
static Result<Vector<String>> split_command_into_args(StringView cmd)
{
return cmd.split(" \n"_sv);
}
static Result<void> 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<void> 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<void> builtin_wrapper(const Vector<String>& args, shell_builtin_t builtin)
{
Vector<const char*> 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<char**>(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<int> execute_loop(bool interactive, SharedPtr<File> 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<void> 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<int> luna_main(int argc, char** argv)
{
@ -97,8 +85,13 @@ Result<int> 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<int> 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);
}