init: migrate from chal repo

This commit is contained in:
2026-06-14 13:41:16 +00:00
commit 4cc47ee53d
6 changed files with 1053 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
CompileFlags:
Add: [-std=c++23, -Wall, -Wextra, -Wpedantic]
Compiler: c++
+14
View File
@@ -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
+13
View File
@@ -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()
+11
View File
@@ -0,0 +1,11 @@
.PHONY: build run clean
build:
cmake -S . -B build
cmake --build build
run: build
./build/catsh
clean:
rm -rf build
+26
View File
@@ -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
View File
@@ -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();
}
}