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 <cstdlib>
#include <filesystem>
#include <vector>
namespace sh
{
@ -13,19 +14,34 @@ namespace sh
exit,
echo,
type,
pwd,
cd,
unknown
};
inline const std::map<const std::string_view, const Command>commandMap
{
{"exit", Command::exit},
{"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);
std::optional<std::filesystem::path> isExec(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,48 +3,140 @@
#include <map>
#include <cstdlib>
#include <filesystem>
#include <vector>
#ifdef _WIN32
#include <process.h>
#else
#include <unistd.h>
#include <sys/wait.h>
#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::vector<std::string> tokens;
std::cout << std::unitbuf;
std::cerr << std::unitbuf;
bool exit {false};
while (!exit)
bool shouldExit {false};
while (!shouldExit)
{
tokens.clear();
std::string input;
std::string line;
bool finshedInput{};
std::cout << "$ ";
std::getline(std::cin, input);
std::optional<std::filesystem::path> result;
switch (sh::Command command = sh::getCommand(input))
while (!finshedInput and std::getline(std::cin, line))
{
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)
auto end = line.size() - 1;
if (line[end] == '\\')
{
std::system(input.c_str());
if (end == 0)
{
std::cout << "> " ;
}
else
{
std::cout << input << ": command not found\n";
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(input.substr(5)WIN_EXEC);
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 <complex>
#include <iostream>
#include <string>
#include <map>
#include <cstdlib>
#include <filesystem>
#ifdef _WIN32
#define HOME "USERPROFILE"
#else
#define HOME "HOME"
#endif
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)
{
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<std::filesystem::path> 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<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";
}
}
}