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
All checks were successful
Build and test / build (push) Successful in 1m46s
This commit is contained in:
parent
646a15d295
commit
6293aeea58
@ -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
153
shell/Command.cpp
Normal 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
36
shell/Command.h
Normal 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
46
shell/Prompt.cpp
Normal 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
14
shell/Prompt.h
Normal 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
20
shell/builtin/echo.cpp
Normal 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 {};
|
||||
};
|
200
shell/main.cpp
200
shell/main.cpp
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user