init: migrate from chal repo
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Add: [-std=c++23, -Wall, -Wextra, -Wpedantic]
|
||||||
|
Compiler: c++
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Local binaries and compiler output
|
||||||
|
/catsh
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# CMake generated files
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
compile_commands.json
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
project(catsh LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
add_executable(catsh src/main.cpp)
|
||||||
|
target_compile_features(catsh PRIVATE cxx_std_23)
|
||||||
|
set_target_properties(catsh PROPERTIES CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
||||||
|
target_compile_options(catsh PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.PHONY: build run clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./build/catsh
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# catsh
|
||||||
|
|
||||||
|
A small (ish) shell written in ( \*hopefully ) modern C++.
|
||||||
|
|
||||||
|
Made while doing the codecrafters' shell implementation challenge -- but let's see I'll add stuff that I find interesting.
|
||||||
|
Given it's at ~1000 LOC the goal is still to keep it as small as possible, as part of that
|
||||||
|
|
||||||
|
- Almost no error handling on POSIX apis or otherwise
|
||||||
|
- Linux only
|
||||||
|
- Edge cases are intended to be unhandled wherever they are
|
||||||
|
- focus on "terse" modern cpp code and practices, even if they are not the most efficient
|
||||||
|
|
||||||
|
what we DO have is
|
||||||
|
|
||||||
|
- no external dependencies ( not even GNU readline ), except POSIX and C++ standard library
|
||||||
|
- support for pipelines, redirection, and background processes
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
`make` wrapper over `cmake`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
make run
|
||||||
|
make clean
|
||||||
|
```
|
||||||
+986
@@ -0,0 +1,986 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <deque>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <ranges>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <termios.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sutils {
|
||||||
|
|
||||||
|
// this resolved based on cwd too, cpp handles that
|
||||||
|
std::optional<std::string> resolve_env_var(std::string_view env_str) {
|
||||||
|
// MUST make a copy as the view might not be null terminated
|
||||||
|
const char *env = std::getenv(std::string{env_str}.data());
|
||||||
|
if (env == nullptr) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
// implicit conversion of return types
|
||||||
|
return std::string{env};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string resolve_path(std::string_view path_str) {
|
||||||
|
auto path = std::filesystem::path(path_str);
|
||||||
|
|
||||||
|
// home path
|
||||||
|
if (path_str.starts_with("~")) {
|
||||||
|
auto home = resolve_env_var("HOME").value();
|
||||||
|
if (path_str.size() > 1) {
|
||||||
|
home += path_str.substr(1);
|
||||||
|
}
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rel path or absol path
|
||||||
|
return std::filesystem::absolute(path).lexically_normal().string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> executable_names_in_path() {
|
||||||
|
auto path_str = resolve_env_var("PATH").value();
|
||||||
|
std::vector<std::string> ret{};
|
||||||
|
for (auto &&p : path_str | std::views::split(':')) {
|
||||||
|
auto path_dir = std::filesystem::path(std::string{p.begin(), p.end()});
|
||||||
|
|
||||||
|
for (auto &&bin : std::filesystem::directory_iterator(path_dir)) {
|
||||||
|
auto bin_path = bin.path();
|
||||||
|
if (bin.is_regular_file() && access(bin_path.c_str(), X_OK) == 0) {
|
||||||
|
ret.push_back(bin_path.filename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> bin_path(std::string_view cmd) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
if (cmd.contains('/')) {
|
||||||
|
auto path = sutils::resolve_path(cmd);
|
||||||
|
return access(path.c_str(), X_OK) == 0 ? std::optional{path} : std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto path_env = sutils::resolve_env_var("PATH").value();
|
||||||
|
|
||||||
|
for (auto dir : path_env | std::views::split(':')) {
|
||||||
|
auto path = fs::path(std::string{dir.begin(), dir.end()}) / cmd;
|
||||||
|
|
||||||
|
if (access(path.c_str(), X_OK) == 0)
|
||||||
|
return path.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string join_cmd(std::span<std::string_view> cmd) {
|
||||||
|
return cmd | std::views::join_with(std::string_view{" "}) |
|
||||||
|
std::ranges::to<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FDs {
|
||||||
|
int fdin, fdout, fderr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// v indicates that this is NOT expected to return
|
||||||
|
[[noreturn]] void exec(std::string path, std::vector<std::string> args) {
|
||||||
|
std::vector<char *> c_args;
|
||||||
|
for (auto &&e : args) {
|
||||||
|
c_args.push_back(e.data());
|
||||||
|
}
|
||||||
|
c_args.push_back(nullptr);
|
||||||
|
|
||||||
|
execv(path.c_str(), c_args.data());
|
||||||
|
std::exit(-1); // in case ^ fails
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t fork_and_exec(std::string path, std::vector<std::string> args, FDs fds) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
// child
|
||||||
|
if (fds.fdin != STDIN_FILENO) {
|
||||||
|
dup2(fds.fdin, STDIN_FILENO);
|
||||||
|
close(fds.fdin);
|
||||||
|
}
|
||||||
|
if (fds.fderr != STDERR_FILENO) {
|
||||||
|
dup2(fds.fderr, STDERR_FILENO);
|
||||||
|
close(fds.fderr);
|
||||||
|
}
|
||||||
|
if (fds.fdout != STDOUT_FILENO) {
|
||||||
|
dup2(fds.fdout, STDOUT_FILENO);
|
||||||
|
close(fds.fdout);
|
||||||
|
}
|
||||||
|
// INFO: why do we care about closing when a process exits
|
||||||
|
// and closes both anyway? well say subprocess redirects to
|
||||||
|
// a different one and closes stdout manually but keeps running still
|
||||||
|
// then I'll still be stuck on a blocking read because desc are open
|
||||||
|
|
||||||
|
exec(path, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedFDRedirect {
|
||||||
|
public:
|
||||||
|
ScopedFDRedirect(const FDs &fds)
|
||||||
|
: old_stdin_(dup(STDIN_FILENO)), old_stdout_(dup(STDOUT_FILENO)),
|
||||||
|
old_stderr_(dup(STDERR_FILENO)) {
|
||||||
|
dup2(fds.fdin, STDIN_FILENO);
|
||||||
|
dup2(fds.fdout, STDOUT_FILENO);
|
||||||
|
dup2(fds.fderr, STDERR_FILENO);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedFDRedirect() {
|
||||||
|
dup2(old_stdin_, STDIN_FILENO);
|
||||||
|
dup2(old_stdout_, STDOUT_FILENO);
|
||||||
|
dup2(old_stderr_, STDERR_FILENO);
|
||||||
|
|
||||||
|
// Only close descriptors this object created with dup().
|
||||||
|
close(old_stdin_);
|
||||||
|
close(old_stdout_);
|
||||||
|
close(old_stderr_);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int old_stdin_;
|
||||||
|
int old_stdout_;
|
||||||
|
int old_stderr_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileFd {
|
||||||
|
public:
|
||||||
|
FileFd(std::string_view path, int flags)
|
||||||
|
: fd_(open(std::string(path).c_str(), flags, 0644)) {}
|
||||||
|
|
||||||
|
~FileFd() { close(fd_); }
|
||||||
|
|
||||||
|
int get() const { return fd_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int fd_;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool is_valid_variable(std::string_view var) {
|
||||||
|
return !var.empty() && !std::isdigit(var[0]) &&
|
||||||
|
std::ranges::all_of(
|
||||||
|
var, [](char c) -> bool { return std::isalnum(c) || c == '_'; });
|
||||||
|
};
|
||||||
|
} // namespace sutils
|
||||||
|
|
||||||
|
namespace sproc {
|
||||||
|
|
||||||
|
using BuiltinArgs = std::span<std::string_view>;
|
||||||
|
// prev I was using std::function wrapper around this but all I really have are
|
||||||
|
// simple functions so just using a fn pointer is simpler
|
||||||
|
using BuiltinFn = void (*)(BuiltinArgs);
|
||||||
|
using Builtin = std::pair<std::string_view, BuiltinFn>;
|
||||||
|
|
||||||
|
void builtin_complete(BuiltinArgs);
|
||||||
|
void builtin_pwd(BuiltinArgs);
|
||||||
|
void builtin_echo(BuiltinArgs);
|
||||||
|
void builtin_cd(BuiltinArgs);
|
||||||
|
void builtin_type(BuiltinArgs);
|
||||||
|
void builtin_jobs(BuiltinArgs);
|
||||||
|
void builtin_exit(BuiltinArgs);
|
||||||
|
void builtin_declare(BuiltinArgs);
|
||||||
|
void builtin_history(BuiltinArgs);
|
||||||
|
|
||||||
|
inline constexpr std::array<Builtin, 9> builtins = {{
|
||||||
|
{"complete", builtin_complete},
|
||||||
|
{"pwd", builtin_pwd},
|
||||||
|
{"echo", builtin_echo},
|
||||||
|
{"type", builtin_type},
|
||||||
|
{"cd", builtin_cd},
|
||||||
|
{"exit", builtin_exit},
|
||||||
|
{"history", builtin_history},
|
||||||
|
{"declare", builtin_declare},
|
||||||
|
{"jobs", builtin_jobs},
|
||||||
|
}};
|
||||||
|
|
||||||
|
auto find_builtin(std::string_view name) {
|
||||||
|
return std::ranges::find(builtins, name, &Builtin::first);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> history;
|
||||||
|
std::size_t history_synced_till = 0;
|
||||||
|
std::map<std::string, std::string, std::less<>> variables;
|
||||||
|
} // namespace sproc
|
||||||
|
|
||||||
|
// TODO: special stuff I don't handle like echo $PATH, * and ? etc etc
|
||||||
|
namespace sread {
|
||||||
|
|
||||||
|
// RAII based Raw mode enabling
|
||||||
|
class WithRawMode {
|
||||||
|
private:
|
||||||
|
struct termios orig_termios_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
~WithRawMode() { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios_); }
|
||||||
|
|
||||||
|
WithRawMode() {
|
||||||
|
tcgetattr(STDIN_FILENO, &orig_termios_);
|
||||||
|
struct termios new_settings = orig_termios_;
|
||||||
|
new_settings.c_lflag &= ~(ICANON | ECHO);
|
||||||
|
// I had a typo earlier where it set on stderr instead but it worked still
|
||||||
|
// because both pointed to the same device ( these settings are applied on
|
||||||
|
// device )
|
||||||
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_settings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<std::string, std::string, std::less<>> completer_scripts;
|
||||||
|
|
||||||
|
struct Completion {
|
||||||
|
std::vector<std::string> matches;
|
||||||
|
std::string lcs;
|
||||||
|
};
|
||||||
|
|
||||||
|
Completion complete_from(std::span<const std::string> candidates,
|
||||||
|
std::string_view prefix) {
|
||||||
|
auto matches = candidates | std::views::filter([&](const auto &candidate) {
|
||||||
|
return candidate.starts_with(prefix);
|
||||||
|
}) |
|
||||||
|
std::ranges::to<std::vector>();
|
||||||
|
|
||||||
|
std::ranges::sort(matches);
|
||||||
|
auto duplicates = std::ranges::unique(matches);
|
||||||
|
matches.erase(duplicates.begin(), duplicates.end());
|
||||||
|
|
||||||
|
if (matches.empty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto lcs = matches.front();
|
||||||
|
for (const auto &match : matches | std::views::drop(1)) {
|
||||||
|
auto [common_end, _] = std::ranges::mismatch(lcs, match);
|
||||||
|
lcs.erase(common_end, lcs.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {std::move(matches), std::move(lcs)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> completion_candidates(std::span<std::string> tokens,
|
||||||
|
const std::string &line) {
|
||||||
|
std::vector<std::string> candidates;
|
||||||
|
|
||||||
|
if (tokens.size() <= 1) {
|
||||||
|
// builtins + path
|
||||||
|
for (auto &&e : sproc::builtins) {
|
||||||
|
candidates.emplace_back(e.first);
|
||||||
|
}
|
||||||
|
auto all = sutils::executable_names_in_path();
|
||||||
|
candidates.append_range(all);
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completer_scripts.contains(tokens[0])) {
|
||||||
|
// i make a pipe, run the script and use the lines
|
||||||
|
int fd[2];
|
||||||
|
pipe(fd);
|
||||||
|
|
||||||
|
std::vector<std::string> args;
|
||||||
|
args.push_back(completer_scripts[tokens[0]]);
|
||||||
|
args.push_back(tokens[0]);
|
||||||
|
// cur token
|
||||||
|
args.push_back(tokens.size() >= 2 ? tokens.back() : "");
|
||||||
|
// prev token
|
||||||
|
args.push_back(tokens.size() >= 2 ? tokens[tokens.size() - 2] : "");
|
||||||
|
|
||||||
|
auto pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
// previously I had a whole map<str,str> for envs supported by
|
||||||
|
// sutils::exec and using execve but this is better as now I "inherit" the
|
||||||
|
// currents vars too and inherit + modify in child is the POSIX model (and
|
||||||
|
// my inherit wasn't implemented prev)
|
||||||
|
auto comp_point = std::to_string(line.size());
|
||||||
|
setenv("COMP_LINE", line.c_str(), 1);
|
||||||
|
setenv("COMP_POINT", comp_point.c_str(), 1);
|
||||||
|
dup2(fd[1], STDOUT_FILENO);
|
||||||
|
close(fd[0]);
|
||||||
|
close(fd[1]);
|
||||||
|
sutils::exec(args[0], args);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(fd[1]); // so as not to block read forever
|
||||||
|
|
||||||
|
std::string lines;
|
||||||
|
char buffer[4096]; // 4kb is the default for pipes so this is it
|
||||||
|
ssize_t n;
|
||||||
|
while ((n = read(fd[0], buffer, sizeof(buffer))) > 0) {
|
||||||
|
// WARN: a lines += buffer which I did earlier is blatantly wrong as
|
||||||
|
// buffer is not null-terminated
|
||||||
|
lines.append(buffer, n);
|
||||||
|
}
|
||||||
|
close(fd[0]);
|
||||||
|
|
||||||
|
waitpid(pid, nullptr, 0);
|
||||||
|
|
||||||
|
for (auto &&chunk : lines | std::views::split('\n')) {
|
||||||
|
if (chunk.begin() != chunk.end())
|
||||||
|
candidates.emplace_back(chunk.begin(), chunk.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete paths from cwd
|
||||||
|
std::string path_prefix = tokens.back();
|
||||||
|
std::string search_dir;
|
||||||
|
auto cwd = std::filesystem::current_path();
|
||||||
|
|
||||||
|
if (path_prefix.contains('/')) {
|
||||||
|
search_dir = sutils::resolve_path(
|
||||||
|
path_prefix.substr(0, path_prefix.find_last_of('/')));
|
||||||
|
} else {
|
||||||
|
search_dir = cwd.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(search_dir)) {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &&e : std::filesystem::directory_iterator(search_dir)) {
|
||||||
|
auto rel_path_str = std::filesystem::relative(e, cwd).string();
|
||||||
|
if (e.is_directory())
|
||||||
|
rel_path_str.push_back('/');
|
||||||
|
candidates.push_back(std::move(rel_path_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> tokenise(std::string_view raw_string) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string tok;
|
||||||
|
char quote = 0;
|
||||||
|
bool building = false;
|
||||||
|
|
||||||
|
auto flush = [&] {
|
||||||
|
if (building) {
|
||||||
|
// even if it's a ${SOME_MISSING_VAR} those are just truncated
|
||||||
|
if (tok.size()) {
|
||||||
|
out.push_back(std::move(tok));
|
||||||
|
tok.clear();
|
||||||
|
}
|
||||||
|
building = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto read_var = [&](size_t &i) -> std::string {
|
||||||
|
auto variable_value = [](std::string_view name) -> std::string {
|
||||||
|
auto it = sproc::variables.find(std::string{name});
|
||||||
|
return it == sproc::variables.end() ? "" : it->second;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (i + 1 >= raw_string.size()) {
|
||||||
|
return "$";
|
||||||
|
}
|
||||||
|
|
||||||
|
// not handling variable name being valid here
|
||||||
|
|
||||||
|
if (raw_string[i + 1] == '{') {
|
||||||
|
size_t start = i + 2;
|
||||||
|
size_t end = raw_string.find('}', start);
|
||||||
|
|
||||||
|
assert(end != std::string_view::npos && "unclosed brace in variable");
|
||||||
|
|
||||||
|
auto name = raw_string.substr(start, end - start);
|
||||||
|
i = end;
|
||||||
|
return variable_value(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t start = i + 1;
|
||||||
|
size_t end = start + 1;
|
||||||
|
// extend till it's valid
|
||||||
|
while (end < raw_string.size() &&
|
||||||
|
(std::isalnum(raw_string[end]) || raw_string[end] == '_')) {
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto name = raw_string.substr(start, end - start);
|
||||||
|
i = end - 1;
|
||||||
|
return variable_value(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < raw_string.size(); ++i) {
|
||||||
|
char c = raw_string[i];
|
||||||
|
|
||||||
|
if (c == '\\' && quote != '\'') {
|
||||||
|
if (++i < raw_string.size())
|
||||||
|
tok += raw_string[i];
|
||||||
|
building = true;
|
||||||
|
} else if (quote) {
|
||||||
|
if (c == quote) {
|
||||||
|
quote = 0;
|
||||||
|
} else if (c == '$' && quote == '"') {
|
||||||
|
tok += read_var(i);
|
||||||
|
building = true;
|
||||||
|
} else {
|
||||||
|
tok += c;
|
||||||
|
building = true;
|
||||||
|
}
|
||||||
|
} else if (c == '\'' || c == '"') {
|
||||||
|
quote = c;
|
||||||
|
building = true;
|
||||||
|
} else if (c == '$') {
|
||||||
|
tok += read_var(i);
|
||||||
|
building = true;
|
||||||
|
} else if (c == ' ') {
|
||||||
|
flush();
|
||||||
|
} else {
|
||||||
|
tok += c;
|
||||||
|
building = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string readline() {
|
||||||
|
// \x makes all the following chars a hex seq but since [ is not hext it
|
||||||
|
// breaks into normal chars -- so this is then 1b as 1 char, then [ an and A
|
||||||
|
// as two more
|
||||||
|
constexpr std::string_view KEY_UP = "\x1b[A";
|
||||||
|
constexpr std::string_view KEY_DOWN = "\x1b[B";
|
||||||
|
constexpr std::string_view PROMPT = "\x1b[38;5;51m~(\x1b[38;5;213m=^‥^\x1b["
|
||||||
|
"38;5;51m)/ \x1b[38;5;118m>\x1b[0m ";
|
||||||
|
constexpr std::string_view BACKSPACE = "\b \b";
|
||||||
|
|
||||||
|
WithRawMode _{};
|
||||||
|
|
||||||
|
std::string raw;
|
||||||
|
char quote = 0;
|
||||||
|
bool last_tab = false;
|
||||||
|
std::string multibye_buffer; // key up and down
|
||||||
|
int history_index = sproc::history.size();
|
||||||
|
|
||||||
|
auto quoted = [&] { return quote != 0; };
|
||||||
|
|
||||||
|
auto erase = [&] {
|
||||||
|
if (raw.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((raw.back() == '\'' || raw.back() == '"') && quote == 0)
|
||||||
|
quote = raw.back(); // imperfect but ok for simple interactive state
|
||||||
|
|
||||||
|
raw.pop_back();
|
||||||
|
std::cout << BACKSPACE;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto erase_all = [&] {
|
||||||
|
// would be wrong for multi-byte chars I think where visual width is not
|
||||||
|
// same as bytes taken
|
||||||
|
for (int i = 0; i < (int)raw.size(); i++)
|
||||||
|
std::cout << BACKSPACE;
|
||||||
|
raw.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto complete = [&] {
|
||||||
|
if (quoted())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto tokens = tokenise(raw);
|
||||||
|
if (raw.empty() || raw.back() == ' ')
|
||||||
|
tokens.emplace_back();
|
||||||
|
|
||||||
|
auto &cur = tokens.back();
|
||||||
|
auto [matches, lcs] =
|
||||||
|
complete_from(completion_candidates(tokens, raw), cur);
|
||||||
|
|
||||||
|
if (lcs.size() > cur.size()) {
|
||||||
|
auto rest = std::string(lcs.begin() + cur.size(), lcs.end());
|
||||||
|
if (matches.size() == 1 && matches[0] == lcs &&
|
||||||
|
!matches[0].ends_with('/'))
|
||||||
|
rest.push_back(' ');
|
||||||
|
raw += rest;
|
||||||
|
std::cout << rest;
|
||||||
|
} else if (last_tab) {
|
||||||
|
std::cout << '\n';
|
||||||
|
|
||||||
|
constexpr std::string_view sep = " ";
|
||||||
|
for (auto &&ch : matches | std::views::join_with(sep))
|
||||||
|
std::cout << ch;
|
||||||
|
|
||||||
|
std::cout << '\n' << PROMPT << raw;
|
||||||
|
} else {
|
||||||
|
std::cout << '\a';
|
||||||
|
}
|
||||||
|
|
||||||
|
last_tab = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto use_history = [&](std::string_view key) -> void {
|
||||||
|
// this below so clamp is valid
|
||||||
|
int hsize = sproc::history.size();
|
||||||
|
if (!hsize)
|
||||||
|
return;
|
||||||
|
int delta = key == KEY_UP ? -1 : 1;
|
||||||
|
history_index += delta;
|
||||||
|
history_index = std::clamp(history_index, 0, hsize);
|
||||||
|
|
||||||
|
// clear and don't populate for the very last strings
|
||||||
|
// ideally it replaces whatever u had partially written but not here
|
||||||
|
erase_all();
|
||||||
|
|
||||||
|
if (history_index < hsize) {
|
||||||
|
auto &&hline = sproc::history[history_index];
|
||||||
|
std::cout << hline;
|
||||||
|
raw = hline;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::cout << PROMPT;
|
||||||
|
|
||||||
|
auto process_char = [&](char c) -> int {
|
||||||
|
switch (c) {
|
||||||
|
case '\n':
|
||||||
|
std::cout << '\n';
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case '\t':
|
||||||
|
complete();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 127:
|
||||||
|
erase();
|
||||||
|
last_tab = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '\'':
|
||||||
|
case '"':
|
||||||
|
quote = quote == c ? 0 : quote == 0 ? c : quote;
|
||||||
|
[[fallthrough]];
|
||||||
|
|
||||||
|
default:
|
||||||
|
raw += c;
|
||||||
|
std::cout << c;
|
||||||
|
last_tab = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (char c; read(STDIN_FILENO, &c, 1) == 1;) {
|
||||||
|
multibye_buffer.push_back(c);
|
||||||
|
|
||||||
|
bool keep_buffering =
|
||||||
|
multibye_buffer.size() < 3 && (KEY_DOWN.starts_with(multibye_buffer) ||
|
||||||
|
KEY_UP.starts_with(multibye_buffer));
|
||||||
|
|
||||||
|
if (!keep_buffering) {
|
||||||
|
if (multibye_buffer == KEY_UP || multibye_buffer == KEY_DOWN) {
|
||||||
|
use_history(multibye_buffer);
|
||||||
|
} else {
|
||||||
|
for (char c : multibye_buffer) {
|
||||||
|
if (process_char(c)) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multibye_buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sread
|
||||||
|
|
||||||
|
namespace sproc {
|
||||||
|
|
||||||
|
void builtin_complete(std::span<std::string_view> tokens) {
|
||||||
|
// unchecked access on tokens
|
||||||
|
if (tokens[0] == "-p") {
|
||||||
|
auto comp_entry = sread::completer_scripts.find(tokens[1]);
|
||||||
|
if (comp_entry != sread::completer_scripts.end()) {
|
||||||
|
std::cout << std::format("complete -C '{}' {}\n", comp_entry->second,
|
||||||
|
tokens[1]);
|
||||||
|
} else {
|
||||||
|
std::cout << "complete: " << tokens[1]
|
||||||
|
<< ": no completion specification\n";
|
||||||
|
}
|
||||||
|
} else if (tokens[0] == "-C") {
|
||||||
|
// [] NEEDS value type to be defalt ctorible but this below
|
||||||
|
// does not, well not that it matters for strings
|
||||||
|
sread::completer_scripts.insert_or_assign(std::string{tokens[2]},
|
||||||
|
std::string{tokens[1]});
|
||||||
|
} else if (tokens[0] == "-r") {
|
||||||
|
sread::completer_scripts.erase(std::string{tokens[1]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_pwd(std::span<std::string_view>) {
|
||||||
|
auto path = std::filesystem::current_path();
|
||||||
|
std::cout << path.string() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_echo(std::span<std::string_view> tokens) {
|
||||||
|
for (char c : tokens | std::views::join_with(' ')) {
|
||||||
|
std::cout << c;
|
||||||
|
}
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_cd(std::span<std::string_view> tokens) {
|
||||||
|
std::string absolute_path = sutils::resolve_path(tokens[0]);
|
||||||
|
if (std::filesystem::exists(absolute_path)) {
|
||||||
|
chdir(absolute_path.c_str());
|
||||||
|
} else {
|
||||||
|
std::cout << "cd: " << absolute_path << ": No such file or directory\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_declare(std::span<std::string_view> tokens) {
|
||||||
|
|
||||||
|
if (tokens[0] == "-p") {
|
||||||
|
auto entry = variables.find(tokens[1]);
|
||||||
|
if (entry == variables.end()) {
|
||||||
|
std::cout << std::format("declare: {}: not found\n", tokens[1]);
|
||||||
|
} else {
|
||||||
|
std::cout << std::format("declare -- {}=\"{}\"\n", entry->first,
|
||||||
|
entry->second);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// expecting a=b
|
||||||
|
auto eq_at = tokens[0].find_last_of('=');
|
||||||
|
auto key = std::string{tokens[0].substr(0, eq_at)};
|
||||||
|
auto val = std::string{tokens[0].substr(eq_at + 1)};
|
||||||
|
assert(!key.empty() && !val.empty() && "no spaces in decls please");
|
||||||
|
if (!sutils::is_valid_variable(key)) {
|
||||||
|
// bash too does this weird backtick then ' quoting
|
||||||
|
std::cout << std::format("declare: `{}': not a valid identifier\n",
|
||||||
|
tokens[0]);
|
||||||
|
} else {
|
||||||
|
variables[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sync_history_from_file(std::filesystem::path path) {
|
||||||
|
std::ifstream file(path);
|
||||||
|
std::string line;
|
||||||
|
while (getline(file, line)) {
|
||||||
|
if (!line.empty())
|
||||||
|
history.push_back(line);
|
||||||
|
}
|
||||||
|
history_synced_till = history.size();
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sync_history_to_file(std::filesystem::path path, bool append) {
|
||||||
|
std::ofstream file(path, append ? std::ios::app : std::ios::trunc);
|
||||||
|
|
||||||
|
auto from = append ? history_synced_till : 0uz;
|
||||||
|
for (; from < history.size(); from++) {
|
||||||
|
file << history[from] << "\n";
|
||||||
|
}
|
||||||
|
history_synced_till = history.size();
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_history(std::span<std::string_view> tokens) {
|
||||||
|
int count = history.size();
|
||||||
|
if (tokens.size()) {
|
||||||
|
if (tokens[0] == "-r") {
|
||||||
|
auto path = sutils::resolve_path(tokens[1]);
|
||||||
|
|
||||||
|
history.clear();
|
||||||
|
// i don't see why we would have this entry but fine ig
|
||||||
|
history.push_back("history " + sutils::join_cmd(tokens));
|
||||||
|
sync_history_from_file(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens[0] == "-w" || tokens[0] == "-a") {
|
||||||
|
auto path = sutils::resolve_path(tokens[1]);
|
||||||
|
sync_history_to_file(path, tokens[0] == "-a");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assuming count as token
|
||||||
|
count = std::min((int)history.size(), std::stoi(std::string{tokens[0]}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = history.size() - count; i < (int)history.size(); i++) {
|
||||||
|
std::cout << std::format("{:5} {}\n", i + 1, history[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_type(std::span<std::string_view> tokens) {
|
||||||
|
if (find_builtin(tokens[0]) != builtins.end()) {
|
||||||
|
std::cout << tokens[0] << " is a shell builtin\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto path = sutils::bin_path(tokens[0])) {
|
||||||
|
std::cout << std::format("{} is {}\n", tokens[0], *path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << tokens[0] << ": not found\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_exit(std::span<std::string_view>) {
|
||||||
|
auto hfile = sutils::resolve_env_var("HISTFILE");
|
||||||
|
if (hfile.has_value())
|
||||||
|
sync_history_to_file(hfile.value(), true);
|
||||||
|
std::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct job_info {
|
||||||
|
int id;
|
||||||
|
pid_t pid;
|
||||||
|
std::string cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::deque<job_info> jobs;
|
||||||
|
|
||||||
|
int next_id() {
|
||||||
|
for (int id = 1;; ++id)
|
||||||
|
if (std::ranges::none_of(jobs, [=](auto &j) { return j.id == id; }))
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reaper() {
|
||||||
|
const auto old_size = std::ssize(jobs);
|
||||||
|
|
||||||
|
for (auto it = jobs.begin(); it != jobs.end();) {
|
||||||
|
const auto i = std::distance(jobs.begin(), it);
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
if (waitpid(it->pid, &status, WNOHANG) <= 0 || !WIFEXITED(status)) {
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char sym = i == old_size - 1 ? '+' : i == old_size - 2 ? '-' : ' ';
|
||||||
|
std::printf("[%d]%c %-24s %s\n", it->id, sym, "Done", it->cmd.c_str());
|
||||||
|
it = jobs.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtin_jobs(std::span<std::string_view>) {
|
||||||
|
size_t i = 0, n = jobs.size();
|
||||||
|
|
||||||
|
std::erase_if(jobs, [&](job_info &j) {
|
||||||
|
int status = 0;
|
||||||
|
bool done = waitpid(j.pid, &status, WNOHANG) > 0 && WIFEXITED(status);
|
||||||
|
|
||||||
|
char sym = i == n - 1 ? '+' : i == n - 2 ? '-' : ' ';
|
||||||
|
|
||||||
|
std::printf("[%d]%c %-24s %s\n", j.id, sym, done ? "Done" : "Running",
|
||||||
|
j.cmd.c_str());
|
||||||
|
|
||||||
|
++i;
|
||||||
|
return done;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_redirect(std::span<std::string_view> &cmd, sutils::FDs &fds,
|
||||||
|
std::optional<sutils::FileFd> &file_fd) {
|
||||||
|
if (cmd.size() < 3)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto op = cmd[cmd.size() - 2];
|
||||||
|
auto file = cmd[cmd.size() - 1];
|
||||||
|
|
||||||
|
bool stdout_redir = op == ">" || op == "1>" || op == ">>" || op == "1>>";
|
||||||
|
bool stderr_redir = op == "2>" || op == "2>>";
|
||||||
|
|
||||||
|
if (!stdout_redir && !stderr_redir)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int flags = O_WRONLY | O_CREAT | (op.ends_with(">>") ? O_APPEND : O_TRUNC);
|
||||||
|
|
||||||
|
file_fd.emplace(sutils::resolve_path(file), flags);
|
||||||
|
|
||||||
|
if (stderr_redir)
|
||||||
|
fds.fderr = file_fd->get();
|
||||||
|
else
|
||||||
|
fds.fdout = file_fd->get();
|
||||||
|
|
||||||
|
cmd = cmd.first(cmd.size() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t launch_command(std::span<std::string_view> cmd, sutils::FDs fds,
|
||||||
|
bool subshell) {
|
||||||
|
std::optional<sutils::FileFd> file_fd;
|
||||||
|
apply_redirect(cmd, fds, file_fd);
|
||||||
|
|
||||||
|
auto args = cmd.subspan(1);
|
||||||
|
|
||||||
|
if (auto it = sproc::find_builtin(cmd[0]); it != sproc::builtins.end()) {
|
||||||
|
if (subshell) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
sutils::ScopedFDRedirect _{fds};
|
||||||
|
it->second(args);
|
||||||
|
std::_Exit(0);
|
||||||
|
} else {
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sutils::ScopedFDRedirect _{fds};
|
||||||
|
it->second(args);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> exec_args;
|
||||||
|
exec_args.reserve(cmd.size());
|
||||||
|
|
||||||
|
for (auto tok : cmd)
|
||||||
|
exec_args.emplace_back(tok);
|
||||||
|
|
||||||
|
auto bin_opt = sutils::bin_path(cmd[0]);
|
||||||
|
|
||||||
|
if (!bin_opt) {
|
||||||
|
std::cout << cmd[0] << ": command not found\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sutils::fork_and_exec(*bin_opt, std::move(exec_args), fds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pipeline(std::span<std::string_view> tokens, bool bg_mode) {
|
||||||
|
std::vector<std::span<std::string_view>> cmds = {};
|
||||||
|
|
||||||
|
auto from = tokens.begin();
|
||||||
|
for (auto cur = tokens.begin(); cur != tokens.end(); cur++) {
|
||||||
|
if (*cur == "|") {
|
||||||
|
cmds.emplace_back(from, cur);
|
||||||
|
from = cur + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmds.emplace_back(from, tokens.end());
|
||||||
|
|
||||||
|
int cmd_cnt = cmds.size();
|
||||||
|
auto fds = std::vector<std::array<int, 2>>(cmd_cnt - 1);
|
||||||
|
for (int i = 0; i < cmd_cnt - 1; i++) {
|
||||||
|
pipe(fds[i].data());
|
||||||
|
|
||||||
|
// INFO: Rem direction as
|
||||||
|
// dup2(a, b) -> make B refer to the same open file as A
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<pid_t> pids;
|
||||||
|
pids.reserve(cmd_cnt);
|
||||||
|
for (int i = 0; i < cmd_cnt; i++) {
|
||||||
|
// i is used only when i > 0
|
||||||
|
sutils::FDs fd = {
|
||||||
|
.fdin = i == 0 ? STDIN_FILENO : fds[i - 1][0],
|
||||||
|
.fdout = i == cmd_cnt - 1 ? STDOUT_FILENO : fds[i][1],
|
||||||
|
.fderr = STDERR_FILENO,
|
||||||
|
};
|
||||||
|
|
||||||
|
auto pid = launch_command(cmds[i], fd, true);
|
||||||
|
// can be 0 in case of cmd not found etc
|
||||||
|
if (pid != 0)
|
||||||
|
pids.push_back(pid);
|
||||||
|
|
||||||
|
// WARN: you NEED this in here else future forks will inherit open
|
||||||
|
// pipes
|
||||||
|
if (i > 0)
|
||||||
|
close(fds[i - 1][0]);
|
||||||
|
if (i < cmd_cnt - 1)
|
||||||
|
close(fds[i][1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bg_mode) {
|
||||||
|
if (!pids.empty()) {
|
||||||
|
sproc::jobs.push_back({
|
||||||
|
.id = sproc::next_id(),
|
||||||
|
.pid = pids.back(),
|
||||||
|
.cmd = sutils::join_cmd(tokens),
|
||||||
|
});
|
||||||
|
std::printf("[%d] %d\n", sproc::jobs.back().id, pids.back());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pid_t pid : pids) {
|
||||||
|
waitpid(pid, nullptr, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_input(std::span<std::string_view> tokens) {
|
||||||
|
history.push_back(sutils::join_cmd(tokens));
|
||||||
|
bool bg_mode = tokens.back() == "&";
|
||||||
|
if (bg_mode)
|
||||||
|
tokens = tokens.first(tokens.size() - 1);
|
||||||
|
if (std::ranges::find(tokens, std::string_view{"|"}) != tokens.end()) {
|
||||||
|
pipeline(tokens, bg_mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pid = launch_command(tokens,
|
||||||
|
{
|
||||||
|
.fdin = STDIN_FILENO,
|
||||||
|
.fdout = STDOUT_FILENO,
|
||||||
|
.fderr = STDERR_FILENO,
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
|
||||||
|
if (pid == 0) // parent
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (bg_mode) {
|
||||||
|
sproc::jobs.push_back({
|
||||||
|
.id = sproc::next_id(),
|
||||||
|
.pid = pid,
|
||||||
|
.cmd = sutils::join_cmd(tokens),
|
||||||
|
});
|
||||||
|
|
||||||
|
std::printf("[%d] %d\n", sproc::jobs.back().id, pid);
|
||||||
|
} else {
|
||||||
|
waitpid(pid, nullptr, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sproc
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::cout << std::unitbuf;
|
||||||
|
std::cerr << std::unitbuf;
|
||||||
|
|
||||||
|
auto hfile = sutils::resolve_env_var("HISTFILE");
|
||||||
|
if (hfile.has_value())
|
||||||
|
sproc::sync_history_from_file(hfile.value());
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
auto input = sread::readline();
|
||||||
|
auto tokens = sread::tokenise(input);
|
||||||
|
|
||||||
|
if (tokens.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto views = tokens | std::views::transform([](const std::string &s) {
|
||||||
|
return std::string_view{s};
|
||||||
|
}) |
|
||||||
|
std::ranges::to<std::vector>();
|
||||||
|
|
||||||
|
sproc::process_input(views);
|
||||||
|
sproc::reaper();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user