added pwd, cd, and now handles escape charaters and quotes, still need to test windows support

This commit is contained in:
Joseph Aquino 2026-01-02 10:37:53 -05:00
parent 87839ef981
commit 328b96a33e
3 changed files with 290 additions and 50 deletions

View File

@ -5,6 +5,7 @@
#include <map> #include <map>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <vector>
namespace sh namespace sh
{ {
@ -13,19 +14,34 @@ namespace sh
exit, exit,
echo, echo,
type, type,
pwd,
cd,
unknown unknown
}; };
inline const std::map<const std::string_view, const Command>commandMap inline const std::map<const std::string_view, const Command>commandMap
{ {
{"exit", Command::exit}, {"exit", Command::exit},
{"echo", Command::echo}, {"echo", Command::echo},
{"type", Command::type} {"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<std::string>& tokens, const std::string& input);
Command getCommand(std::string_view input); Command getCommand(std::string_view input);
std::optional<std::filesystem::path> isExec(const std::string& input); std::optional<std::filesystem::path> isExec(const std::string& input);
void printType(const std::string& input); void printType(const std::string& input);
std::filesystem::path getWorkingDir();
void changeDir(const std::vector<std::string>& tokens);
} }

View File

@ -3,49 +3,141 @@
#include <map> #include <map>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <vector>
#ifdef _WIN32
#include <process.h>
#else
#include <unistd.h>
#include <sys/wait.h>
#endif
#include "shellUtils.h" #include "shellUtils.h"
#ifdef WIN32
#define WIN_EXEC .append(".exe") // executables on windows need .exe to run
#else
#define WIN_EXEC
#endif
int main() int main()
{ {
std::cout << std::unitbuf; std::vector<std::string> tokens;
std::cerr << std::unitbuf;
bool exit {false}; std::cout << std::unitbuf;
while (!exit) 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; auto end = line.size() - 1;
std::cout << "$ "; if (line[end] == '\\')
std::getline(std::cin, input); {
std::optional<std::filesystem::path> result; if (end == 0)
switch (sh::Command command = sh::getCommand(input))
{ {
case sh::Command::exit: std::cout << "> " ;
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;
} }
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<std::filesystem::path> 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<char*> args;
args.push_back(const_cast<char*>(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<char*>(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;
}
}
} }

View File

@ -1,17 +1,90 @@
#include "shellUtils.h" #include "shellUtils.h"
#include <complex>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <map> #include <map>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#ifdef _WIN32
#define HOME "USERPROFILE"
#else
#define HOME "HOME"
#endif
namespace sh namespace sh
{ {
void tokenize(std::vector<std::string>& 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) Command getCommand(std::string_view input)
{ {
if (commandMap.contains(input. if (commandMap.contains(input.substr(0, input.find(' '))))
substr(0, input.find(' '))))
{ {
return commandMap.at(input.substr(0, input.find(' '))); return commandMap.at(input.substr(0, input.find(' ')));
} }
@ -24,36 +97,44 @@ namespace sh
std::optional<std::filesystem::path> isExec(const std::string& input) std::optional<std::filesystem::path> isExec(const std::string& input)
{ {
const std::string_view pathEnv {std::getenv("PATH")}; std::string pathEnv {std::getenv("PATH")};
#if WIN32 #ifdef _WIN32
constexpr char delimiter {';'}; constexpr char delimiter {';'};
#else #else
constexpr char delimiter {':'}; constexpr char delimiter {':'};
#endif #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 startCharIndex{};
size_t nextDelimIndex{pathEnv.find(delimiter)}; size_t nextDelimIndex{pathEnv.find(delimiter)};
std::filesystem::path currentPath;
while (nextDelimIndex != std::string::npos) while (nextDelimIndex != std::string::npos)
{ {
std::filesystem::path currentPath {pathEnv.substr(startCharIndex, nextDelimIndex - startCharIndex)};
std::filesystem::path fullPath; currentPath = pathEnv.substr(startCharIndex, nextDelimIndex - startCharIndex);
fullPath = currentPath / input; currentPath /= input;
namespace fs = std::filesystem; namespace fs = std::filesystem;
if (!fs::exists(fullPath)) if (!fs::exists(currentPath))
{ {
startCharIndex = nextDelimIndex + 1; startCharIndex = nextDelimIndex + 1;
nextDelimIndex = pathEnv.find(delimiter, startCharIndex); nextDelimIndex = pathEnv.find(delimiter, startCharIndex);
continue; 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; startCharIndex = nextDelimIndex + 1;
nextDelimIndex = pathEnv.find(delimiter, startCharIndex); nextDelimIndex = pathEnv.find(delimiter, startCharIndex);
continue; continue;
} }
return fullPath; return currentPath;
} }
return std::nullopt; return std::nullopt;
} }
@ -70,11 +151,62 @@ namespace sh
if (const auto fullPath {isExec(input)}) if (const auto fullPath {isExec(input)})
{ {
std::cout << input; std::cout << input;
std::cout << " is " << fullPath.value() << "\n"; std::cout << " is " << fullPath.value().string() << "\n";
return; return;
} }
std::cout << input; std::cout << input;
std::cout << ": not found\n"; std::cout << ": not found\n";
} }
void changeDir(const std::vector<std::string>& 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";
}
}
} }