From 418059acde4e38dd4b23cb8073a13da5972c172c Mon Sep 17 00:00:00 2001 From: Venelin Date: Sat, 7 Oct 2023 19:38:27 +0300 Subject: [PATCH] init --- basicTypes.h | 15 +++ childProcess.cpp | 189 +++++++++++++++++++++++++++++++++ childProcess.hpp | 52 ++++++++++ commands.cpp | 264 +++++++++++++++++++++++++++++++++++++++++++++++ commands.hpp | 160 ++++++++++++++++++++++++++++ cppipe.cpp | 128 +++++++++++++++++++++++ install.sh | 20 ++++ test.cpp | 62 +++++++++++ 8 files changed, 890 insertions(+) create mode 100644 basicTypes.h create mode 100644 childProcess.cpp create mode 100644 childProcess.hpp create mode 100644 commands.cpp create mode 100644 commands.hpp create mode 100644 cppipe.cpp create mode 100755 install.sh create mode 100644 test.cpp diff --git a/basicTypes.h b/basicTypes.h new file mode 100644 index 0000000..b905f81 --- /dev/null +++ b/basicTypes.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +typedef int8_t I8; +typedef uint8_t U8; +typedef int16_t I16; +typedef uint16_t U16; +typedef int32_t I32; +typedef uint32_t U32; +typedef int64_t I64; +typedef uint64_t U64; + +typedef int fd_t; // file descriptor diff --git a/childProcess.cpp b/childProcess.cpp new file mode 100644 index 0000000..1092833 --- /dev/null +++ b/childProcess.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include +#include "childProcess.hpp" + + +RetProc::RetProc(Proc origin, int status) + : Proc(origin) + , normal_exit(WIFEXITED(status)) + , returned(WEXITSTATUS(status)) +{} + +RetProc::operator bool() +{ + return normal_exit && returned == 0; +} + +/* create a process taking input from pipe childIn */ +static Proc createProc(const char* const argv[], const fd_t childIn[2]) +{ + Proc childproc; + childproc.in = childIn[1]; + childproc.out = STDOUT_FILENO; + + childproc.pid = fork(); + if(childproc.pid == 0) /* child */ + { + close(childIn[1]); + dup2(childIn[0], STDIN_FILENO); + + exec_or_die(argv); + } + + return childproc; +} + +/* create a process and redirect it's output to a pipe */ +static Proc createRedirProc(const char* const argv[]) +{ + fd_t childOut[2]; + pipe(childOut); + + Proc childproc; + childproc.in = STDIN_FILENO; + childproc.out = childOut[0]; + + childproc.pid = fork(); + /* close(STDIN_FILENO); flush?*/ + if(childproc.pid != 0) /* parent */ + { + close(childOut[1]); + } + else + { + dup2(childOut[1], STDOUT_FILENO); + + exec_or_die(argv); + } + + return childproc; +} + +/* create a process redirecting both in and out */ +static Proc createRedirProc(const char* const argv[], const fd_t childIn[2]) +{ + fd_t childOut[2]; /* todo: add error #include "processTypes.hpp"*/ + pipe(childOut); + + Proc childproc; + childproc.in = childIn[1]; + childproc.out = childOut[0]; + + childproc.pid = fork(); + /* close(STDIN_FILENO); flush?*/ + if(childproc.pid != 0) /* parent */ + { + close(childOut[1]); + } + else + { + close(childIn[1]); + dup2(childIn[0], STDIN_FILENO); + + dup2(childOut[1], STDOUT_FILENO); + + exec_or_die(argv); + } + + return childproc; +} + + +Proc createRedirProcess(const char* const argv[], U32 flags) /* todo rename */ +{ + fd_t childIn[2]; + Proc proc; + + if(flags & INPUT) + { + pipe(childIn); + if(flags & OUTPUT) + proc = createRedirProc(argv, childIn); + else + proc = createProc(argv, childIn); + } + else if(flags & OUTPUT) + proc = createRedirProc(argv); + else + proc = createProcess(argv); + + return proc; +} + +Proc createCapProcess(const char* const argv[], fd_t in, fd_t err) +{ + fd_t childOut[2]; + pipe(childOut); + + Proc proc; + proc.in = in; + proc.out = childOut[0]; + proc.err = err; + + proc.pid = fork(); + if(proc.pid == 0) /* child */ + { + if(in != STDIN_FILENO) + dup2(in, STDIN_FILENO); + + dup2(childOut[1], STDOUT_FILENO); + + if(err != STDERR_FILENO) + dup2(err, STDERR_FILENO); + + exec_or_die(argv); + } + else + { + close(childOut[1]); + } + + return proc; +} + +Proc createProcess(const char* const argv[], fd_t in, fd_t out, fd_t err) +{ + Proc proc; + proc.in = in; + proc.out = out; + proc.err = err; + + proc.pid = fork(); + if(proc.pid == 0) /* child */ + { + if(in != STDIN_FILENO) + dup2(in, STDIN_FILENO); + + if(out != STDOUT_FILENO) + dup2(out, STDOUT_FILENO); + + if(err != STDERR_FILENO) + dup2(err, STDERR_FILENO); + + exec_or_die(argv); + } + + return proc; +} + +RetProc runProcess(const char* const argv[], fd_t in, fd_t out, fd_t err) +{ + Proc p = createProcess(argv, in, out, err); + + // Wait to end + int status; + waitpid(p.pid, &status, 0); + + return RetProc(p, status); +} + +void exec_or_die(const char* const argv[]) +{ + execvp(argv[0], (char* const *)argv); + + std::cerr << "Can't execute: " << argv[0] << ' ' << strerror(errno) << std::endl; + _exit(1); // _exit since we are a child +} diff --git a/childProcess.hpp b/childProcess.hpp new file mode 100644 index 0000000..6ae42fc --- /dev/null +++ b/childProcess.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "basicTypes.h" + +struct Proc +{ + pid_t pid; + fd_t in; + fd_t out; + fd_t err; +}; + +struct RetProc: public Proc /* returned process */ +{ + RetProc(Proc, int status); /* status as returned by waitpid */ + bool normal_exit; + U8 returned; /* return code */ + + /* A returned process evaluates to true if it exited normaly and + * returned 0 */ + explicit operator bool(); + +}; + +enum Redirect: U32 +{ + NOTHING =0, + INPUT = 1 << 0, + OUTPUT = 1 << 1, + ERR = 1 << 2 /* todo */ +}; + + +/* For all funcions argv is an array of command parameters + * argv[0] is the command itself, argv needs to end with nullptr */ + +/* Create a proccess and redirect anyting flaged to a new pipe + flags - INPUT, OUTPUT, ERR + return pipe file descriptors*/ +Proc createRedirProcess(const char* const argv[], U32 flags); + +/* Capture ouput to a pipe, can take alternative input and err FDs */ +Proc createCapProcess(const char* const argv[], fd_t in=0, fd_t err=2); + +/* Can take FDs as arguments which will be used instead of std */ +Proc createProcess(const char* const argv[], fd_t in=0, fd_t out=1, fd_t err=2); + +/* Same but waits for the process to finish */ +RetProc runProcess(const char* const argv[], fd_t in=0, fd_t out=1, fd_t err=2); + +/* execvp the command or exit */ +void exec_or_die(const char* const argv[]); diff --git a/commands.cpp b/commands.cpp new file mode 100644 index 0000000..5d6ba7c --- /dev/null +++ b/commands.cpp @@ -0,0 +1,264 @@ +#include +#include +#include +#include +#include +#include +#include +#include "commands.hpp" +#include "childProcess.hpp" + +enum constants: I32 +{ + FILE_PERMISIONS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH +}; + +static fd_t open_or_die(const char* file, I32 flags) +{ + fd_t fd = open(file, flags, FILE_PERMISIONS); + if(fd == -1) + { + std::cerr << "Can't open: " << file << ' ' << strerror(errno) << std::endl; + exit(1); + } + return fd; +} + +RetProc Cmd::operator()(fd_t in, fd_t out, fd_t err) +{ + RetProc p = runProcess(argv.data(), in, out, err); + + // Close files if such were used + // if(in > 2) + // close(in); + // if(out > 2) + // close(out); + // if(err > 2) + // close(err); + + return p; +} + +Proc Cmd::detach(fd_t in, fd_t out, fd_t err) +{ + return createProcess(argv.data(), in, out, err); +} + +void Cmd::append_args(std::initializer_list args) +{ + argv.reserve(argv.size() + args.size()); + argv.back() = *args.begin(); + for(auto it = args.begin() + 1; it < args.end(); ++it) + argv.push_back(*it); + argv.push_back(nullptr); +} + +Cmd Cmd::operator+(const char* arg) +{ + Cmd result(*this); + result.argv.back() = arg; + result.argv.push_back(nullptr); + return result; +} + +Cmd& Cmd::operator+=(const char* arg) +{ + argv.back() = arg; + argv.push_back(nullptr); + return *this; +} + + +PendingCmd::PendingCmd(const Cmd& origin, fd_t in, fd_t out, fd_t err) + : cmd(origin) + , in(in) + , out(out) + , err(err) + , execed_(false) +{} + +PendingCmd::~PendingCmd() +{ + if(!execed_) + operator()(); + /* if(in != 0) */ + /* close(in); */ + /* if(out != 1) */ + /* close(out); */ + /* if(err != 2) */ + /* close(err); */ +} + +RetProc PendingCmd::operator()() +{ + assert(!execed_ && "Executed command twice"); + execed_ = true; + return cmd(in, out, err); +} + +RetProc PendingCmd::runRedir() +{ + assert(!execed_ && "executed command twice"); assert(out==1 && "already redirected"); + execed_ = true; + + RetProc p = runProcess(cmd.argv.data(), in, err); + return p; +} + +Proc PendingCmd::detach() +{ + assert(!execed_ && "Executed command twice"); + execed_ = true; + return cmd.detach(in, out, err); +} + +Proc PendingCmd::detachRedirOut() +{ + assert(!execed_ && "Executed command twice"); assert(out==1 && "already redirected"); + execed_ = true; + return createCapProcess(cmd.argv.data(), in, err); +} + +void PendingCmd::cancel() +{ + execed_ = true; +} + +str $(const PendingCmd& cmd) +{ + RetProc p = const_cast(cmd).runRedir(); + + // check output size (not portable?) + int pipe_size; + int rc = ioctl(p.out, FIONREAD, &pipe_size); assert(rc==0); + + // write to the string directly, todo: find a better way + str output; + output.resize(pipe_size); + read(p.out, (char*)output.c_str(), output.size()); + return output; +} + +void exec(const Cmd& cmd) +{ + exec_or_die(cmd.argv.data()); +} + +PendingCmd operator,(const PendingCmd& cfirst, const Cmd& second) +{ + auto& first = const_cast(cfirst); + first(); + return PendingCmd(second); +} + +PendingCmd operator,(RetProc, const Cmd& second) +{ + return PendingCmd(second); +} + +PendingCmd operator|(const PendingCmd& cfirst, const Cmd& second) +{ + auto& first = const_cast(cfirst); + fd_t firstOut = first.detachRedirOut().out; + return PendingCmd(second, firstOut); +} + + +RetProc operator&&(const PendingCmd& cfirst, const Cmd& csecond) +{ + auto& first = const_cast(cfirst); + auto& second = const_cast(csecond); + RetProc firstProc = first(); + + if(firstProc) + return second(); + + return firstProc; +} + +RetProc operator&&(RetProc p, const Cmd& ccmd) +{ + auto& cmd = const_cast(ccmd); + if(p) + return cmd(); + + return p; +} + +RetProc operator||(const PendingCmd& cfirst, const Cmd& csecond) +{ + auto& first = const_cast(cfirst); + auto& second = const_cast(csecond); + RetProc firstProc = first(); + + if(!firstProc) + return second(); + + return firstProc; +} + +RetProc operator||(RetProc p, const Cmd& ccmd) +{ + auto& cmd = const_cast(ccmd); + if(!p) + return cmd(); + + return p; +} + +PendingCmd& operator>(const PendingCmd& cmd, const char* file) +{ + fd_t fd = open_or_die(file, O_WRONLY | O_CREAT); + return cmd > fd; +} +PendingCmd& operator>(const PendingCmd& ccmd, fd_t fd) +{ + auto& cmd = const_cast(ccmd); + assert(cmd.out == 1 || !"ERROR: Output is already redirected!"); + + cmd.out = fd; + return cmd; +} + +PendingCmd& operator>>(const PendingCmd& cmd, const char* file) +{ + fd_t fd = open_or_die(file, O_WRONLY | O_CREAT | O_APPEND); + return cmd > fd; +} +PendingCmd& operator>>(const PendingCmd& cmd, fd_t fd) +{ + return cmd > fd; +} + +PendingCmd& operator>=(const PendingCmd& cmd, const char* file) +{ + fd_t fd = open_or_die(file, O_WRONLY | O_CREAT); + return cmd >= fd; +} +PendingCmd& operator>=(const PendingCmd& ccmd, fd_t fd) +{ + auto& cmd = const_cast(ccmd); + assert(cmd.err == 2 || !"ERROR: Error output is already redirected!"); + + cmd.err = fd; + return cmd; +} + +PendingCmd& operator<(const PendingCmd& cmd, const char* file) +{ + fd_t fd = open_or_die(file, O_RDONLY); + return cmd < fd; +} +PendingCmd& operator<(const PendingCmd& ccmd, fd_t fd) +{ + auto& cmd = const_cast(ccmd); + assert(cmd.in == 0 || !"ERROR: Input is already redirected!"); + cmd.in = fd; + return cmd; +} + +PendingCmd operator&(const PendingCmd& cfirst, const Cmd& second) +{ + const_cast(cfirst).detach(); + return PendingCmd(second); +} diff --git a/commands.hpp b/commands.hpp new file mode 100644 index 0000000..652a135 --- /dev/null +++ b/commands.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include "childProcess.hpp" + +/* All CONST references are used and cast away to allow for taking + both l and r values without using templates or making copies + + implicitly convert to it but not to const char* and we want the + implicit conversion for quick scripting + arg pointers are kept and used not copied */ + +/* A shell command */ +class Cmd +{ +public: + /* all args must be const char* */ + template + explicit Cmd(Args... args) + : argv({args...}) + { + argv.push_back(nullptr); + } + + /* Execute the command, if arguments are not given use stdin,out,err + else use the given file desciptors. this can also be used + to redirect err to out by giving err=1 like in shell */ + RetProc operator()(fd_t in=0, fd_t out=1, fd_t err=2); + + /* Run the command, don't wait to return like shell's & */ + Proc detach(fd_t in=0, fd_t out=1, fd_t err=2); + + /* Append arguments */ + void append_args(std::initializer_list); + /* Append an argument and return the new command */ + Cmd operator+(const char* arg); + /* Append an argument */ + Cmd& operator+=(const char* arg); + + std::vector argv; /* null terminateded arg list */ +}; + +/* An instance of a shell comand that is pending execution + if the command is not executed during the life of the obj + its executed on destruction */ +class PendingCmd +{ +public: + /* Can be implicitly created from a command */ + PendingCmd(const Cmd&, fd_t in=0, fd_t out=1, fd_t err=2); + /* Execute the command on destruction */ + ~PendingCmd(); + + /* No copy we use unnamed return value optimization to return PendingCmd without destruction */ + PendingCmd(const PendingCmd&) = delete; + PendingCmd& operator=(const PendingCmd&) = delete; + + /* Run the command */ + RetProc operator()(); + /* Run the command, capturing the output in a new pipe */ + RetProc runRedir(); + + /* Run the command async + shell: cmd & + becomes: cmd.detach() */ + Proc detach(); + /* Detach but redirect output to a pipe */ + Proc detachRedirOut(); + + /* Prevent a pending command from being executed on destruction */ + void cancel(); + + Cmd cmd; + fd_t in, out, err; +private: + bool execed_; +}; + + +/* Anything that takes PendingCmd can take Cmd aswell */ + +/* Like shell's exec */ +void exec(const Cmd&); /* todo: take Pending? */ + +/* Run commands in sequence + shell: + ls + cd + becomes: + ls, + cd; +*/ +PendingCmd operator,(const PendingCmd&, const Cmd&); +PendingCmd operator,(RetProc, const Cmd&); +/* Forbiden funcion to prevent wrong sequencing such as: + echo, echo && echo + here the 2nd and 3rd echos would be ran before 1st + due to the C++ operator precedence*/ +PendingCmd operator,(const PendingCmd&, RetProc) = delete; + + +/* Shell pipe operator | - execute two commands, second takes input + from first can be chained many times */ +PendingCmd operator|(const PendingCmd&, const Cmd&); + +/* Shell operator && - run the second command only if first returns 0 (no errors)*/ +RetProc operator&&(const PendingCmd&, const Cmd&); +RetProc operator&&(RetProc, const Cmd&); + +/* Shell operator || - run second only if first returns != 0 + Operator precedence is different from shell, C precendence is && > || + so mixing || and && may not compile, but shouldn't cause other issues, + use () to resolve these cases */ +RetProc operator||(const PendingCmd&, const Cmd&); +RetProc operator||(RetProc, const Cmd&); + +/* Shell operator > - redirect output to file + shell: cmd > file 2>&1 + becomes: cmd > "file" >=1 */ +PendingCmd& operator>(const PendingCmd&, const char* file); +/* A file descriptor can be given istead of a file path in which case + * no truncation occurs */ +PendingCmd& operator>(const PendingCmd&, fd_t); +/* Same but append rather then truncate the file */ +PendingCmd& operator>>(const PendingCmd&, const char* file); +PendingCmd& operator>>(const PendingCmd&, fd_t); +/* Redirect errors to file */ +PendingCmd& operator>=(const PendingCmd&, const char* file); +PendingCmd& operator>=(const PendingCmd&, fd_t); +/* todo >>= */ + +/* Shell oprator < use file as input */ +PendingCmd& operator<(const PendingCmd&, const char* file); +PendingCmd& operator<(const PendingCmd&, fd_t); + +/* Shell operator & but only for single processes + e.g.: echo a && echo b & echo c + in shell would detach "echo a && echo b" but here + this is not possible */ +PendingCmd operator&(const PendingCmd&, const Cmd&); + +/* string that is implicitly cast to const char* and doesn't deallocate + to allow for simpler synthax, speed and safety + e.g: echo + some_string + rather then: echo + some_string.c_str() */ +class str: public std::basic_string /* todo: allocator */ +{ +public: + using std::basic_string::basic_string; + operator const char*() { return c_str();} +}; + +/* Execute a command and capture the output + shell: + var=$(ls) + becomes: + string var = $(ls); +*/ +str $(const PendingCmd&); diff --git a/cppipe.cpp b/cppipe.cpp new file mode 100644 index 0000000..c3b90a2 --- /dev/null +++ b/cppipe.cpp @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include +#include "commands.hpp" + +using namespace std; +using namespace std::filesystem; + +static path get_cache_path(const char* file); +static bool compile_file(const char* file, path cache, bool debug); // return true on success + +int main(int argc, char* argv[]) +{ + if(argc < 2) + { + cout << "Usage: cppipe [-g] FILE [ARGUMENTS]...\n"; + return 1; + } + if(!strcmp(argv[1], "--help")) + { + cout << "Usage: cppipe [-g] FILE [ARGUMENTS]...\n" + << "Compile and run C++ source FILE that uses the cppipe library.\n" + "Pass the ARGUMENTS to he compiled binary.\n" + "The binaries are cached and recompiled only if the source is newer.\n" + "-g debug the binary, asserts are also enabled\n"; + return 0; + + } + + bool debug = false; + int file_arg = 1; + if(!strcmp(argv[1], "-g")) + { + if(argc < 3) + { + cout << "Usage: cppipe [-g] FILE [ARGUMENTS]...\n"; + return 1; + } + + debug = true; + file_arg = 2; + } + + const char* file = argv[file_arg]; + if( !exists(file) ) + { + cerr << "File: " << file << " doesn't exist\n"; + exit(1); + } + + path cache( get_cache_path(file) ); // cache bins to avoid recompiles + + if(debug) + cache += "_dbg"; + + // Compile the passed file + if( !compile_file(file, cache, debug) ) + return 1; + + // Run the file without forking + Cmd run; + if(debug) // debug it with gdb + { + run += "gdb"; + run += "--args"; + } + + run += cache.c_str(); + for(int i = file_arg; i < argc; ++i) + run += argv[i]; + exec(run); +} + +static path get_cache_path(const char* file) +{ + path cache; + if(char* xdg_cache = getenv("XDG_CACHE_HOME")) + { + cache = xdg_cache; + cache /= "cppipe"; + } + else + { + char* home( getenv("HOME") ); + cache = home; + cache /= ".cache/cppipe"; + } + cache += absolute(file); + create_directories(cache.parent_path()); + return cache; +} + +static bool compile_file(const char* file, path cache, bool debug) +{ + // Only compile if the source is newer then the bin or we are in debug + error_code ec; + if(last_write_time(file) > last_write_time(cache, ec)) + { + Cmd compile( + "g++", + "-o", cache.c_str(), + "-pipe", "-std=c++17", "-march=native", + // "-Wall", "-Wextra", "-Wno-parentheses", + file, + "-lcppipe" + ); + + if(debug) + { + compile.append_args({ + "-g", + "-Wall", "-Wextra", "-Wno-parentheses" + }); + } + else + { + compile.append_args({ + "-Ofast", "-flto", "-DNDEBUG", "-s" + }); + } + + if( !compile() ) // if failed to compile + return false; + } + return true; +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..f784306 --- /dev/null +++ b/install.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +# Compile lib +options="-std=c++17 -Ofast -flto -march=native -DNDEBUG -Wall -Wextra -Wno-parentheses -s -pipe" +g++ -c $options commands.cpp childProcess.cpp + +# Create lib +ar r libcppipe.a commands.o childProcess.o +mv libcppipe.a /usr/local/lib +rm commands.o childProcess.o + +# Compile cppipe +g++ -o cppipe $options cppipe.cpp -lcppipe +chmod 755 cppipe +mv cppipe /usr/local/bin + +# Add headers +mkdir -p /usr/local/include/cppipe +cp basicTypes.h commands.hpp childProcess.hpp /usr/local/include/cppipe diff --git a/test.cpp b/test.cpp new file mode 100644 index 0000000..2953579 --- /dev/null +++ b/test.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +// tab vs space +// shaded obj? +// fix warning +/* close files on destruction ?*/ +/* -> for error ? */ +// check if processes normal_exit + +using namespace std; +int main() +{ + // f(var("ab")); + Cmd ls("ls"); + Cmd ll("ls", "-l"); + Cmd grep("grep"); + Cmd echo("echo"); + Cmd rm("rm"); + + str out1 = $(ll | grep + "cpp" | grep + "child"); + if(out1.find("childProcess.cpp") != str::npos) + cout << "OK 0/11" << endl; + + Cmd success("echo", "OK 1/11"); + Cmd fail("mkdir", "."); + Cmd unexpected("echo", "FAILURE"); + success && + fail && + unexpected; + + // shouldnt compile + // success || + // unexpected && + // unexpected; + + Cmd write_file("echo", "Existing ", " ", "file. OK 2/11"); + + write_file > "file.txt"; + grep + "Existing" < "file.txt"; + + echo + "Appended to file OK 3/11" >> "file.txt"; + grep + "Appended" < "file.txt" && + rm + "file.txt" && + fail || + Cmd("echo", "OK 4/11"); + + echo + "OK 5/11" && + echo + "OK 6/11", + Cmd("echo", "OK 7/11"); + + echo + "OK 8/11" & + echo + "OK 9/11" && + echo + "OK 10/11"; + + // wait for all detached + while(wait(nullptr) != -1); + + exec( echo + $(echo + "OK 11/11") ); +}