diff --git a/include/hyprutils/cli/ArgumentParser.hpp b/include/hyprutils/cli/ArgumentParser.hpp new file mode 100644 index 0000000..7ae9746 --- /dev/null +++ b/include/hyprutils/cli/ArgumentParser.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "../memory/UniquePtr.hpp" + +namespace Hyprutils::CLI { + class CArgumentParserImpl; + + class CArgumentParser { + public: + CArgumentParser(const std::span& args); + ~CArgumentParser() = default; + + std::expected registerBoolOption(std::string&& name, std::string&& abbrev, std::string&& description); + std::expected registerIntOption(std::string&& name, std::string&& abbrev, std::string&& description); + std::expected registerFloatOption(std::string&& name, std::string&& abbrev, std::string&& description); + std::expected registerStringOption(std::string&& name, std::string&& abbrev, std::string&& description); + + std::optional getBool(const char* name); + std::optional getInt(const char* name); + std::optional getFloat(const char* name); + std::optional getString(const char* name); + + // commence the parsing after registering + std::expected parse(); + + std::string getDescription(const std::string_view& header, std::optional maxWidth = {}); + + private: + Memory::CUniquePointer m_impl; + }; +}; \ No newline at end of file diff --git a/include/hyprutils/string/String.hpp b/include/hyprutils/string/String.hpp index 3bcf8c9..841f2db 100644 --- a/include/hyprutils/string/String.hpp +++ b/include/hyprutils/string/String.hpp @@ -9,5 +9,6 @@ namespace Hyprutils { std::string_view trim(const std::string_view& in); bool isNumber(const std::string& str, bool allowfloat = false); void replaceInString(std::string& string, const std::string& what, const std::string& to); + bool truthy(const std::string_view& in); }; }; \ No newline at end of file diff --git a/src/cli/ArgumentParser.cpp b/src/cli/ArgumentParser.cpp new file mode 100644 index 0000000..41bb93a --- /dev/null +++ b/src/cli/ArgumentParser.cpp @@ -0,0 +1,287 @@ +#include "ArgumentParser.hpp" + +#include +#include + +#include +#include + +using namespace Hyprutils::CLI; +using namespace Hyprutils::Memory; +using namespace Hyprutils::String; +using namespace Hyprutils; + +CArgumentParser::CArgumentParser(const std::span& args) : m_impl(makeUnique(args)) { + ; +} + +std::expected CArgumentParser::registerBoolOption(std::string&& name, std::string&& abbrev, std::string&& description) { + return m_impl->registerOption(std::move(name), std::move(abbrev), std::move(description), ARG_TYPE_BOOL); +} + +std::expected CArgumentParser::registerIntOption(std::string&& name, std::string&& abbrev, std::string&& description) { + return m_impl->registerOption(std::move(name), std::move(abbrev), std::move(description), ARG_TYPE_INT); +} + +std::expected CArgumentParser::registerFloatOption(std::string&& name, std::string&& abbrev, std::string&& description) { + return m_impl->registerOption(std::move(name), std::move(abbrev), std::move(description), ARG_TYPE_FLOAT); +} + +std::expected CArgumentParser::registerStringOption(std::string&& name, std::string&& abbrev, std::string&& description) { + return m_impl->registerOption(std::move(name), std::move(abbrev), std::move(description), ARG_TYPE_STR); +} + +std::optional CArgumentParser::getBool(const char* name) { + auto ref = m_impl->getValue(name); + + if (ref == m_impl->m_values.end()) + return std::nullopt; + + if (const auto pval = std::get_if(&ref->val); pval) + return *pval; + + return std::nullopt; +} + +std::optional CArgumentParser::getInt(const char* name) { + auto ref = m_impl->getValue(name); + + if (ref == m_impl->m_values.end()) + return std::nullopt; + + if (const auto pval = std::get_if(&ref->val); pval) + return *pval; + + return std::nullopt; +} + +std::optional CArgumentParser::getFloat(const char* name) { + auto ref = m_impl->getValue(name); + + if (ref == m_impl->m_values.end()) + return std::nullopt; + + if (const auto pval = std::get_if(&ref->val); pval) + return *pval; + + return std::nullopt; +} + +std::optional CArgumentParser::getString(const char* name) { + auto ref = m_impl->getValue(name); + + if (ref == m_impl->m_values.end()) + return std::nullopt; + + if (const auto pval = std::get_if(&ref->val); pval) + return *pval; + + return std::nullopt; +} + +std::string CArgumentParser::getDescription(const std::string_view& header, std::optional maxWidth) { + return m_impl->getDescription(header, maxWidth); +} + +std::expected CArgumentParser::parse() { + return m_impl->parse(); +} + +CArgumentParserImpl::CArgumentParserImpl(const std::span& args) { + m_argv.reserve(args.size()); + for (const auto& a : args) { + m_argv.emplace_back(a); + } +} + +std::vector::iterator CArgumentParserImpl::getValue(const std::string_view& sv) { + auto it = std::ranges::find_if(m_values, [&sv](const auto& e) { return e.full == sv || e.abbrev == sv; }); + return it; +} + +std::expected CArgumentParserImpl::registerOption(std::string&& name, std::string&& abbrev, std::string&& description, eArgumentType type) { + if (getValue(name) != m_values.end() || getValue(abbrev) != m_values.end()) + return std::unexpected("Value already exists"); + + m_values.emplace_back(SArgumentKey{ + .full = std::move(name), + .abbrev = std::move(abbrev), + .desc = std::move(description), + .argType = type, + .val = std::monostate{}, + }); + + return {}; +} + +std::expected CArgumentParserImpl::parse() { + // walk the args + for (size_t i = 1; i < m_argv.size(); ++i) { + auto val = m_values.end(); + const auto& arg = m_argv.at(i); + + if (arg.starts_with("--")) + val = getValue(std::string_view{arg}.substr(2)); + else if (arg.starts_with('-')) + val = getValue(std::string_view{arg}.substr(1)); + else + return std::unexpected(std::format("Invalid element found while parsing: {}", arg)); + + if (val == m_values.end()) + return std::unexpected(std::format("Invalid argument found while parsing: {}", arg)); + + switch (val->argType) { + case ARG_TYPE_BOOL: { + val->val = true; + break; + } + case ARG_TYPE_INT: { + if (i + 1 >= m_argv.size()) + return std::unexpected(std::format("Failed parsing argument {}, no value supplied", arg)); + const auto& next = std::string{m_argv.at(++i)}; + if (!isNumber(next)) + return std::unexpected(std::format("Failed parsing argument {}, value {} is not an int", arg, next)); + try { + val->val = sc(std::stoi(next)); + } catch (...) { return std::unexpected(std::format("Failed parsing argument {}, value {} is not an int", arg, next)); } + break; + } + case ARG_TYPE_FLOAT: { + if (i + 1 >= m_argv.size()) + return std::unexpected(std::format("Failed parsing argument {}, no value supplied", arg)); + const auto& next = std::string{m_argv.at(++i)}; + if (!isNumber(next, true)) + return std::unexpected(std::format("Failed parsing argument {}, value {} is not a float", arg, next)); + try { + val->val = sc(std::stof(next)); + } catch (...) { return std::unexpected(std::format("Failed parsing argument {}, value {} is not a float", arg, next)); } + break; + } + case ARG_TYPE_STR: { + if (i + 1 >= m_argv.size()) + return std::unexpected(std::format("Failed parsing argument {}, no value supplied", arg)); + val->val = std::string{m_argv.at(++i)}; + break; + } + + case ARG_TYPE_END: break; + } + } + + return {}; +} + +std::string CArgumentParserImpl::getDescription(const std::string_view& header, std::optional maxWidth) { + + const size_t MAX_COLS = maxWidth.value_or(80); + const std::string PAD_STR = " "; + + constexpr const std::array TYPE_STRS = { + "", // bool + "[int]", // int + "[float]", // float + "[str]", // str + }; + + // + auto wrap = [](const std::string_view& str, size_t maxW) -> std::vector { + std::vector result; + + // walk word by word + size_t nextSpacePos = 0, lastBreakPos = 0; + while (true) { + size_t lastSpacePos = nextSpacePos; + nextSpacePos = str.find(' ', nextSpacePos + 1); + + if (nextSpacePos == std::string::npos) + break; + + if (nextSpacePos - lastBreakPos > maxW) { + if (lastSpacePos - lastBreakPos <= maxW) { + // break + result.emplace_back(str.substr(lastBreakPos, lastSpacePos - lastBreakPos)); + lastBreakPos = lastSpacePos + 1; + } else { + while (lastSpacePos - lastBreakPos > maxW) { + // break + result.emplace_back(str.substr(lastBreakPos, maxW)); + lastBreakPos += maxW; + } + } + continue; + } + } + + result.emplace_back(str.substr(lastBreakPos)); + + return result; + }; + + auto pad = [&PAD_STR](size_t len) -> std::string_view { + if (len >= PAD_STR.size()) + return PAD_STR; + return std::string_view{PAD_STR}.substr(0, len); + }; + + std::string rolling; + rolling += std::format("┏ {}\n", header); + rolling += "┣"; + for (size_t i = 0; i < MAX_COLS; ++i) { + rolling += "━"; + } + rolling += "┓\n"; + + // get max widths + size_t maxArgWidth = 0, maxShortWidth = 0; + for (const auto& v : m_values) { + maxShortWidth = std::max(maxShortWidth, v.abbrev.size() + 4 + std::string_view{TYPE_STRS[v.argType]}.length()); + maxArgWidth = std::max(maxArgWidth, v.full.size() + 3); + } + + // write the table + for (const auto& v : m_values) { + size_t lenUsed = 0; + rolling += "┣ --" + v.full; + lenUsed += 3 + v.full.size(); + rolling += pad(maxArgWidth - lenUsed); + lenUsed = maxArgWidth; + + rolling += " -" + v.abbrev; + lenUsed += 2 + v.abbrev.size(); + rolling += " "; + rolling += TYPE_STRS[v.argType]; + lenUsed += std::string_view{TYPE_STRS[v.argType]}.length() + 1; + rolling += pad(maxArgWidth + maxShortWidth - lenUsed); + lenUsed = maxArgWidth + maxShortWidth; + + rolling += " | "; + lenUsed += 3; + + const auto ROWS = wrap(v.desc, MAX_COLS - lenUsed); + + const auto LEN_START_DESC = lenUsed; + + rolling += ROWS[0]; + lenUsed += ROWS[0].size(); + rolling += pad(MAX_COLS - lenUsed); + rolling += "┃\n"; + + for (size_t i = 1; i < ROWS.size(); ++i) { + lenUsed = LEN_START_DESC; + rolling += "┣"; + rolling += pad(LEN_START_DESC); + rolling += ROWS[i]; + lenUsed += ROWS[i].size(); + rolling += pad(MAX_COLS - lenUsed); + rolling += "┃\n"; + } + } + + rolling += "┗"; + for (size_t i = 0; i < MAX_COLS; ++i) { + rolling += "━"; + } + rolling += "┛\n"; + + return rolling; +} diff --git a/src/cli/ArgumentParser.hpp b/src/cli/ArgumentParser.hpp new file mode 100644 index 0000000..ff33b5e --- /dev/null +++ b/src/cli/ArgumentParser.hpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include + +namespace Hyprutils::CLI { + enum eArgumentType : uint8_t { + ARG_TYPE_BOOL = 0, + ARG_TYPE_INT, + ARG_TYPE_FLOAT, + ARG_TYPE_STR, + ARG_TYPE_END, + }; + + struct SArgumentKey { + using Value = std::variant; + + std::string full, abbrev, desc; + eArgumentType argType = ARG_TYPE_BOOL; + + Value val; + }; + + class CArgumentParserImpl { + public: + CArgumentParserImpl(const std::span& args); + ~CArgumentParserImpl() = default; + + std::string getDescription(const std::string_view& header, std::optional maxWidth = {}); + std::expected parse(); + std::vector::iterator getValue(const std::string_view& sv); + std::expected registerOption(std::string&& name, std::string&& abbrev, std::string&& description, eArgumentType type); + + std::vector m_values; + + std::vector m_argv; + }; +} \ No newline at end of file diff --git a/src/string/String.cpp b/src/string/String.cpp index db8dcc1..8028e34 100644 --- a/src/string/String.cpp +++ b/src/string/String.cpp @@ -88,3 +88,16 @@ void Hyprutils::String::replaceInString(std::string& string, const std::string& pos += to.length(); } } + +bool Hyprutils::String::truthy(const std::string_view& in) { + if (in == "1") + return true; + + if (in == "0") + return false; + + std::string lower = std::string{in}; + std::ranges::transform(lower, lower.begin(), ::tolower); + + return lower.starts_with("true") || lower.starts_with("yes") || lower.starts_with("on"); +} diff --git a/tests/cli/ArgumentParser.cpp b/tests/cli/ArgumentParser.cpp new file mode 100644 index 0000000..fec00b2 --- /dev/null +++ b/tests/cli/ArgumentParser.cpp @@ -0,0 +1,103 @@ +#include + +#include + +#include + +using namespace Hyprutils::CLI; +using namespace Hyprutils; + +constexpr const char* DESC_TEST = R"#(┏ My description +┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┣ --hello -h | Says hello ┃ +┣ --hello2 -e | Says hello 2 ┃ +┣ --value -v [float] | Sets a valueeeeeee ┃ +┣ --longlonglonglongintopt -l [int] | Long long ┃ +┣ maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa┃ +┣ aaaaaaaaaaan maaan man maaan man maaan ┃ +┣ man maaan man ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +)#"; + +TEST(CLI, ArgumentParser) { + std::vector argv = {"app", "--hello", "--value", "0.2"}; + + CArgumentParser parser(argv); + + EXPECT_TRUE(parser.registerBoolOption("hello", "h", "Says hello")); + EXPECT_TRUE(parser.registerBoolOption("hello2", "e", "Says hello 2")); + EXPECT_TRUE(parser.registerFloatOption("value", "v", "Sets a valueeeeeee")); + EXPECT_TRUE(parser.registerIntOption("longlonglonglongintopt", "l", "Long long maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan maaan man maaan man maaan man maaan man")); + + auto result = parser.parse(); + + EXPECT_TRUE(result.has_value()); + + std::println("{}", parser.getDescription("My description")); + + if (!result.has_value()) + std::println("Error: {}", result.error()); + + EXPECT_EQ(parser.getBool("hello").value_or(false), true); + EXPECT_EQ(parser.getBool("hello2").value_or(false), false); + EXPECT_EQ(parser.getFloat("value").value_or(0.F), 0.2F); + + EXPECT_EQ(parser.getDescription("My description"), DESC_TEST); + + CArgumentParser parser2(argv); + + EXPECT_TRUE(parser2.registerBoolOption("hello2", "e", "Says hello 2")); + EXPECT_TRUE(parser2.registerFloatOption("value", "v", "Sets a valueeeeeee")); + EXPECT_TRUE(parser2.registerIntOption("longlonglonglongintopt", "l", "Long long maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan maaan man maaan man maaan man maaan man")); + + auto result2 = parser2.parse(); + + EXPECT_TRUE(!result2.has_value()); + + std::vector argv3 = {"app", "--hello", "--value"}; + + CArgumentParser parser3(argv3); + + EXPECT_TRUE(parser3.registerBoolOption("hello2", "e", "Says hello 2")); + EXPECT_TRUE(parser3.registerFloatOption("value", "v", "Sets a valueeeeeee")); + EXPECT_TRUE(parser3.registerIntOption("longlonglonglongintopt", "l", "Long long maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan maaan man maaan man maaan man maaan man")); + + auto result3 = parser3.parse(); + + EXPECT_TRUE(!result3.has_value()); + + std::vector argv4 = {"app", "--value", "hi", "-w", "2"}; + + CArgumentParser parser4(argv4); + + EXPECT_TRUE(parser4.registerStringOption("value", "v", "Sets a valueeeeeee")); + EXPECT_TRUE(parser4.registerIntOption("value2", "w", "Sets a valueeeeeee 2")); + + auto result4 = parser4.parse(); + + EXPECT_TRUE(result4.has_value()); + + EXPECT_EQ(parser4.getString("value").value_or(""), "hi"); + EXPECT_EQ(parser4.getInt("value2").value_or(0), 2); + + std::vector argv5 = { + "app", + "e", + }; + + CArgumentParser parser5(argv5); + + EXPECT_TRUE(parser5.registerStringOption("value", "v", "Sets a valueeeeeee")); + EXPECT_TRUE(parser5.registerStringOption("value2", "w", "Sets a valueeeeeee 2")); + + auto result5 = parser5.parse(); + + EXPECT_TRUE(!result5.has_value()); + + CArgumentParser parser6(argv5); + + EXPECT_TRUE(parser6.registerStringOption("aa", "v", "Sets a valueeeeeee")); + EXPECT_TRUE(!parser6.registerStringOption("aa", "w", "Sets a valueeeeeee 2")); + EXPECT_TRUE(parser6.registerStringOption("bb", "b", "Sets a valueeeeeee")); + EXPECT_TRUE(!parser6.registerStringOption("cc", "b", "Sets a valueeeeeee 2")); +} \ No newline at end of file diff --git a/tests/string/String.cpp b/tests/string/String.cpp index f60214a..5a0d668 100644 --- a/tests/string/String.cpp +++ b/tests/string/String.cpp @@ -31,4 +31,17 @@ TEST(String, string) { EXPECT_EQ(isNumber("-1.0", true), true); EXPECT_EQ(isNumber("-1..0", true), false); EXPECT_EQ(isNumber("-10.0000000001", true), true); + + EXPECT_EQ(truthy("frgeujgeruibger"), false); + EXPECT_EQ(truthy("false"), false); + EXPECT_EQ(truthy("0"), false); + EXPECT_EQ(truthy("yees"), false); + EXPECT_EQ(truthy("naa"), false); + EXPECT_EQ(truthy("-1"), false); + EXPECT_EQ(truthy("true"), true); + EXPECT_EQ(truthy("true eeee ee"), true); + EXPECT_EQ(truthy("yesss"), true); + EXPECT_EQ(truthy("1"), true); + EXPECT_EQ(truthy("on"), true); + EXPECT_EQ(truthy("onn"), true); } \ No newline at end of file