init: migrate from chal repo
This commit is contained in:
+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