diff --git a/include/shellUtils.h b/include/shellUtils.h index d510d84..7b5f501 100644 --- a/include/shellUtils.h +++ b/include/shellUtils.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace sh { @@ -13,19 +14,34 @@ namespace sh exit, echo, type, + pwd, + cd, unknown }; + inline const std::mapcommandMap { - {"exit", Command::exit}, - {"echo", Command::echo}, - {"type", Command::type} + {"exit", Command::exit}, + {"echo", Command::echo}, + {"type", Command::type}, + {"pwd" , Command::pwd }, + {"cd" , Command::cd } }; + inline constexpr std::string_view doubleQuoteEscapeChars = "\"$`\n\\"; + + inline constexpr std::string_view tokenDelimiters = " \t"; + + void tokenize(std::vector& tokens, const std::string& input); + Command getCommand(std::string_view input); std::optional isExec(const std::string& input); void printType(const std::string& input); + + std::filesystem::path getWorkingDir(); + + void changeDir(const std::vector& tokens); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 104121f..f88e21d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,49 +3,141 @@ #include #include #include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif #include "shellUtils.h" -#ifdef WIN32 -#define WIN_EXEC .append(".exe") // executables on windows need .exe to run -#else -#define WIN_EXEC -#endif - int main() { - std::cout << std::unitbuf; - std::cerr << std::unitbuf; + std::vector tokens; - bool exit {false}; - while (!exit) + std::cout << std::unitbuf; + std::cerr << std::unitbuf; + + bool shouldExit {false}; + while (!shouldExit) + { + tokens.clear(); + + std::string input; + std::string line; + bool finshedInput{}; + + std::cout << "$ "; + + while (!finshedInput and std::getline(std::cin, line)) { - std::string input; - std::cout << "$ "; - std::getline(std::cin, input); - std::optional result; - switch (sh::Command command = sh::getCommand(input)) + auto end = line.size() - 1; + if (line[end] == '\\') + { + if (end == 0) { - case sh::Command::exit: - exit = true; - break; - case sh::Command::echo: - std::cout << input.substr(5) << "\n"; - break; - case sh::Command::unknown: - result = sh::isExec(input.substr(0, input.find(' '))WIN_EXEC); - if (result) - { - std::system(input.c_str()); - } - else - { - std::cout << input << ": command not found\n"; - } - break; - case sh::Command::type: - sh::printType(input.substr(5)WIN_EXEC); - break; + std::cout << "> " ; } + else + { + input += line.substr(0, line.size() - 1); + std::cout << "> "; + } + } + else + { + input += line; + finshedInput = true; + } } + + //trim whitespace and tabs from start and end + const size_t start = input.find_first_not_of(" \t"); + if (start == std::string::npos) + {// the input was all whitespace and tabs + input.clear(); + } + else + { + const size_t end = input.find_last_not_of(" \t"); + input = input.substr(start, end - start + 1); + } + + if (input.empty()) + { + continue; + } + + //we can now assume there is actual text from the input + sh::tokenize(tokens, input); + + std::optional result; + + switch (sh::Command command = sh::getCommand(tokens[0])) + { + case sh::Command::exit: + shouldExit = true; + break; + case sh::Command::echo: + for (int i = 1; i < tokens.size(); i++) + { + std::cout << tokens[i] << " "; + } + std::cout << "\n"; + break; + case sh::Command::unknown: + result = sh::isExec(tokens[0]); + if (result) + { + std::vector args; + args.push_back(const_cast(result.value().filename().c_str()));// have exe path on seperate element for execv + + for (size_t i = 1; i < tokens.size(); i++) + { + args.push_back(const_cast(tokens[i].c_str())); + } + + args.push_back(nullptr); +#ifdef _WIN32 + intptr_t status = _spawnv(_P_WAIT, result.value().c_str(), args.data()); + + if (status == -1) + { + perror("\n\nError executing program\n\n"); + return 1; + } +#else + pid_t pid = fork(); + if (pid == 0) + {// child process + execv(result.value().c_str(), args.data()); + perror("\n\nError executing program\n\n"); + exit(1); + } + else + { + int status; + waitpid(pid, &status, 0); + } +#endif + } + else + { + std::cout << tokens[0] << ": command not found\n"; + } + break; + case sh::Command::type: + sh::printType(tokens[1]); + break; + case sh::Command::pwd: + std::cout << std::filesystem::current_path().string() << "\n"; + break; + case sh::Command::cd: + sh::changeDir(tokens); + break; + } + } } diff --git a/src/shellUtils.cpp b/src/shellUtils.cpp index 5a698b7..48342ee 100644 --- a/src/shellUtils.cpp +++ b/src/shellUtils.cpp @@ -1,17 +1,90 @@ #include "shellUtils.h" +#include #include #include #include #include #include +#ifdef _WIN32 +#define HOME "USERPROFILE" +#else +#define HOME "HOME" +#endif + + namespace sh { + void tokenize(std::vector& tokens, const std::string& input) + { + bool inSingleQuote{false}; + bool inDoubleQuote{false}; + + std::string currentToken; + for (size_t i = 0; i < input.size(); i++) + { + const char c = input[i]; + if (c == '\\' and !inSingleQuote) + { + if (i + 1 < input.size()) + { + if (inDoubleQuote and (doubleQuoteEscapeChars.find(input[i+1]) == std::string::npos)) + { + currentToken += input[i]; + } + currentToken += input[i+1]; + i++; + } + continue; + } + + if (c == '\'' and !inDoubleQuote) + { + inSingleQuote = !inSingleQuote; + continue; + } + + if (c == '\"' and !inSingleQuote) + { + inDoubleQuote = !inDoubleQuote; + continue; + } + + if ((tokenDelimiters.find(c) != std::string::npos) and !inSingleQuote and !inDoubleQuote) + { + if (!currentToken.empty()) + { + tokens.emplace_back(currentToken); + currentToken.clear(); + } + continue; + } + currentToken += c; + } + + if (!currentToken.empty()) + { + tokens.emplace_back(currentToken); + } + +// std::istringstream tokens {input}; +// std::string currentToken; +// while (tokens >> currentToken) +// { +// // check for escaped white space +// if (currentToken.back() == '\\') +// {//add whitespace to token +// currentToken.append(1, ' '); +// } +// tokenContainer.emplace_back(currentToken); +// } + + } + Command getCommand(std::string_view input) { - if (commandMap.contains(input. - substr(0, input.find(' ')))) + if (commandMap.contains(input.substr(0, input.find(' ')))) { return commandMap.at(input.substr(0, input.find(' '))); } @@ -24,36 +97,44 @@ namespace sh std::optional isExec(const std::string& input) { - const std::string_view pathEnv {std::getenv("PATH")}; -#if WIN32 + std::string pathEnv {std::getenv("PATH")}; +#ifdef _WIN32 constexpr char delimiter {';'}; #else constexpr char delimiter {':'}; #endif + //add our current path, put a delimiter so std::string::find doesn't skip the last token + pathEnv.append(1, delimiter); + pathEnv.append(std::filesystem::current_path().string()); + pathEnv.append(1, delimiter); + size_t startCharIndex{}; size_t nextDelimIndex{pathEnv.find(delimiter)}; + std::filesystem::path currentPath; + while (nextDelimIndex != std::string::npos) { - std::filesystem::path currentPath {pathEnv.substr(startCharIndex, nextDelimIndex - startCharIndex)}; - std::filesystem::path fullPath; - fullPath = currentPath / input; + + currentPath = pathEnv.substr(startCharIndex, nextDelimIndex - startCharIndex); + currentPath /= input; + namespace fs = std::filesystem; - if (!fs::exists(fullPath)) + if (!fs::exists(currentPath)) { startCharIndex = nextDelimIndex + 1; nextDelimIndex = pathEnv.find(delimiter, startCharIndex); continue; } - if ((fs::status(fullPath).permissions() & fs::perms::owner_exec) != fs::perms::owner_exec) - { + if ((fs::status(currentPath).permissions() & fs::perms::owner_exec) != fs::perms::owner_exec) + {// fullPath exists but cant executed startCharIndex = nextDelimIndex + 1; nextDelimIndex = pathEnv.find(delimiter, startCharIndex); continue; } - return fullPath; + return currentPath; } return std::nullopt; } @@ -70,11 +151,62 @@ namespace sh if (const auto fullPath {isExec(input)}) { std::cout << input; - std::cout << " is " << fullPath.value() << "\n"; + std::cout << " is " << fullPath.value().string() << "\n"; return; } std::cout << input; std::cout << ": not found\n"; } + + void changeDir(const std::vector& tokens) + { + if (tokens.size() > 2) + { + std::cerr << "cd: too many arguments\n"; + return; + } + + if (tokens.size() < 2) + {// no arguments go home + #if _WIN32 + return; + #endif + const char* result = nullptr; + result = std::getenv(HOME); + if (result == nullptr) //might not have home defined + { + result = "/"; + } + std::filesystem::current_path(result); + return; + } + + std::filesystem::path expandedPath = tokens[1]; + if (tokens[1].front() == '~') + { + const char* result = nullptr; + result = std::getenv(HOME); + if (result == nullptr) //might not have home defined + { + result = "/"; + } + expandedPath = result; + if (tokens[1].size() > 1) + { + expandedPath /= tokens[1].substr(2); + } + } + + if (std::filesystem::is_directory(expandedPath)) + { + std::filesystem::current_path(expandedPath); + } + else + { + std::cerr << "cd: " << expandedPath.string() << ": No such file or directory\n"; + } + + } + }