diff --git a/src/util/argparser.cpp b/src/util/argparser.cpp new file mode 100644 index 0000000..63b9968 --- /dev/null +++ b/src/util/argparser.cpp @@ -0,0 +1,568 @@ +/* + * Copyright (C) 2021-2022 Riyyi + * + * SPDX-License-Identifier: MIT + */ + +#include // find_if +#include // size_t +#include // uint8_t +#include // printf +#include // strcmp +#include // numeric_limits +#include // stod, stoi, stoul +#include +#include + +#include "util/argparser.h" + +namespace Util { + +ArgParser::ArgParser() +{ +} + +ArgParser::~ArgParser() +{ +} + +void ArgParser::printError(char parameter, Error error) +{ + char tmp[] { parameter, '\0' }; + printError(tmp, error, false); +} + +void ArgParser::printError(const char* parameter, Error error, bool longName) +{ + if (!m_errorReporting) { + return; + } + + if (error == Error::OptionInvalid) { + printf("%s: invalid option -- '%s'\n", m_name, parameter); + } + else if (error == Error::OptionUnrecognized) { + printf("%s: unrecognized option -- '%s'\n", m_name, parameter); + } + else if (error == Error::OptionDoesntAllowArgument) { + printf("%s: option '--%s' doesn't allow an argument\n", m_name, parameter); + } + else if (error == Error::OptionRequiresArgument) { + if (longName) { + printf("%s: option '--%s' requires an argument\n", m_name, parameter); + } + else { + printf("%s: option requires an argument -- '%s'\n", m_name, parameter); + } + } + else if (error == Error::OptionInvalidArgumentType) { + if (longName) { + printf("%s: option '--%s' invalid type\n", m_name, parameter); + } + else { + printf("%s: option invalid type -- '%s'\n", m_name, parameter); + } + } + else if (error == Error::ArgumentExtraOperand) { + printf("%s: extra operand '%s'\n", m_name, parameter); + } + else if (error == Error::ArgumentRequired) { + printf("%s: missing required argument '%s'\n", m_name, parameter); + } + else if (error == Error::ArgumentInvalidType) { + printf("%s: invalid argument type '%s'\n", m_name, parameter); + } + + // TODO: Print command usage, if it's enabled. +} + +// Required: directly after || separated by space +// Optional: directly after +bool ArgParser::parseShortOption(std::string_view option, std::string_view next) +{ + bool result = true; + +#ifndef NDEBUG + printf("Parsing short option: '%s'\n", option.data()); +#endif + + char c; + std::string_view value; + for (size_t i = 0; i < option.size(); ++i) { + c = option.at(i); + +#ifndef NDEBUG + printf("short '%c'\n", c); +#endif + + auto foundOption = std::find_if(m_options.begin(), m_options.end(), [&c](Option& it) -> bool { + return it.shortName == c; + }); + + // Option does not exist + if (foundOption == m_options.cend()) { + printError(c, Error::OptionInvalid); + + if (m_exitOnFirstError) { + return false; + } + } + else if (foundOption->requiresArgument == Required::No) { + // FIXME: Figure out why providing a nullptr breaks the lambda here. + result = foundOption->acceptValue(""); + } + else if (foundOption->requiresArgument == Required::Yes) { + value = option.substr(i + 1); + if (value.empty() && next.empty()) { + foundOption->error = Error::OptionRequiresArgument; + printError(c, Error::OptionRequiresArgument); + result = false; + } + else if (!value.empty()) { + result = foundOption->acceptValue(value.data()); + if (!result) { + printError(c, Error::OptionInvalidArgumentType); + } + } + else if (next[0] == '-') { + foundOption->error = Error::OptionRequiresArgument; + printError(c, Error::OptionRequiresArgument); + result = false; + } + else { + result = foundOption->acceptValue(next.data()); + m_optionIndex++; + if (!result) { + printError(c, Error::OptionInvalidArgumentType); + } + } + + break; + } + else if (foundOption->requiresArgument == Required::Optional) { + value = option.substr(i + 1); + if (!value.empty()) { + result = foundOption->acceptValue(value.data()); + if (!result) { + printError(c, Error::OptionInvalidArgumentType); + } + break; + } + } + } + + return result; +} + +// Required: directly after, separated by '=' || separated by space +// Optional: directly after, separated by '=' +bool ArgParser::parseLongOption(std::string_view option, std::string_view next) +{ + std::string name = std::string(option.substr(0, option.find_first_of('='))); + std::string_view value = option.substr(option.find_first_of('=') + 1); + + auto foundOption = std::find_if(m_options.begin(), m_options.end(), [&name](Option& it) -> bool { + return it.longName && it.longName == name; + }); + + if (foundOption == m_options.cend()) { + printError(name.data(), Error::OptionUnrecognized); + return false; + } + + enum class ArgumentProvided : uint8_t { + No, + DirectlyAfter, + Seperated, + }; + + auto argument = ArgumentProvided::No; + if (name != value || option.find('=') != std::string_view::npos) { + argument = ArgumentProvided::DirectlyAfter; + } + else if (!next.empty() && next[0] != '-') { + argument = ArgumentProvided::Seperated; + value = next; + } + + bool result = true; + + if (foundOption->requiresArgument == Required::No) { + if (argument == ArgumentProvided::DirectlyAfter) { + foundOption->error = Error::OptionDoesntAllowArgument; + printError(name.data(), Error::OptionDoesntAllowArgument); + return false; + } + + result = foundOption->acceptValue(""); + } + else if (foundOption->requiresArgument == Required::Yes) { + if (argument == ArgumentProvided::No) { + foundOption->error = Error::OptionRequiresArgument; + printError(name.data(), Error::OptionRequiresArgument); + return false; + } + + result = foundOption->acceptValue(value.data()); + if (!result) { + printError(name.data(), Error::OptionInvalidArgumentType); + } + + if (argument == ArgumentProvided::Seperated) { + m_optionIndex++; + } + } + else if (foundOption->requiresArgument == Required::Optional) { + if (argument == ArgumentProvided::DirectlyAfter) { + result = foundOption->acceptValue(value.data()); + if (!result) { + printError(name.data(), Error::OptionInvalidArgumentType); + } + } + } + + return result; +} + +bool ArgParser::parseArgument(std::string_view argument) +{ + bool result = true; + + for (;;) { + // Run out of argument handlers + if (m_argumentIndex >= m_arguments.size()) { + printError(argument.data(), Error::ArgumentExtraOperand); + return false; + } + + Argument& currentArgument = m_arguments.at(m_argumentIndex); + result = currentArgument.acceptValue(argument.data()); + + if (result) { + currentArgument.addedValues++; + if (currentArgument.addedValues >= currentArgument.maxValues) { + m_argumentIndex++; + } + } + else if (currentArgument.minValues == 0) { + m_argumentIndex++; + continue; + } + else { + printError(argument.data(), Error::ArgumentInvalidType); + } + + break; + } + + return result; +} + +bool ArgParser::parse(int argc, const char* argv[]) +{ + bool result = true; + + // Set looping indices + m_optionIndex = 1; + m_argumentIndex = 0; + + // By default parse all '-' prefixed parameters as options + m_nonOptionMode = false; + + // Get program name + m_name = argv[0] + std::string_view(argv[0]).find_last_of('/') + 1; + + std::string_view argument; + std::string_view next; + for (; m_optionIndex < (size_t)argc; ++m_optionIndex) { + +#ifndef NDEBUG + printf("argv[%zu]: %s\n", m_optionIndex, argv[m_optionIndex]); +#endif + + // Get the current and next parameter + argument = argv[m_optionIndex]; + if (m_optionIndex + 1 < (size_t)argc && argv[m_optionIndex + 1][0] != '-') { + next = argv[m_optionIndex + 1]; + } + else { + next = {}; + } + + // Stop parsing '-' prefixed parameters as options afer '--' + if (argument.compare("--") == 0) { + m_nonOptionMode = true; + continue; + } + + // Long Option + if (!m_nonOptionMode && argument[0] == '-' && argument[1] == '-') { + argument = argument.substr(argument.find_first_not_of('-')); + if (!parseLongOption(argument, next)) { + result = false; + } + } + // Short Option + else if (!m_nonOptionMode && argument[0] == '-') { + argument = argument.substr(argument.find_first_not_of('-')); + if (!parseShortOption(argument, next)) { + result = false; + } + } + // Argument + else { + if (m_stopParsingOnFirstNonOption) { + m_nonOptionMode = true; + } + if (!parseArgument(argument)) { + result = false; + } + } + + if (m_exitOnFirstError && !result) { + break; + } + } + + // Check any leftover arguments for required + for (; m_argumentIndex < m_arguments.size(); ++m_argumentIndex) { + Argument& currentArgument = m_arguments.at(m_argumentIndex); + if (currentArgument.minValues > currentArgument.addedValues) { + result = false; + printError(currentArgument.name ? currentArgument.name : "", Error::ArgumentRequired); + } + } + + if (result) { + return true; + } + + for (auto& option : m_options) { + if (option.longName && strcmp(option.longName, "help") == 0) { + printf("Try '%s --help' for more information.\n", m_name); + break; + } + if (option.shortName == 'h') { + printf("Try '%s -h' for more information.\n", m_name); + break; + } + } + + return false; +} + +// ----------------------------------------- + +void ArgParser::addOption(Option&& option) +{ + m_options.push_back(option); +} + +void ArgParser::addOption(bool& value, char shortName, const char* longName, const char* usageString, const char* manString) +{ + addOption({ shortName, longName, nullptr, usageString, manString, Required::No, getAcceptFunction(value) }); +} + +void ArgParser::addOption(const char*& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) }); +} + +void ArgParser::addOption(std::string& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) }); +} + +void ArgParser::addOption(std::string_view& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) }); +} + +void ArgParser::addOption(int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) }); +} + +void ArgParser::addOption(unsigned int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) }); +} + +void ArgParser::addOption(double& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) }); +} + +void ArgParser::addOption(std::vector& values, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(values) }); +} + +// ----------------------------------------- + +void ArgParser::addArgument(Argument&& argument) +{ + m_arguments.push_back(argument); +} + +void ArgParser::addArgument(bool& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(const char*& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(std::string& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(std::string_view& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(int& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(unsigned int& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(double& value, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) }); +} + +void ArgParser::addArgument(std::vector& values, const char* name, const char* usageString, const char* manString, Required required) +{ + size_t minValues = required == Required::Yes ? 1 : 0; + addArgument({ name, usageString, manString, minValues, values.max_size(), 0, getAcceptFunction(values) }); +} + +// ----------------------------------------- + +AcceptFunction ArgParser::getAcceptFunction(bool& value) +{ + return [&value](const char*) -> bool { + value = true; + return true; + }; +} + +AcceptFunction ArgParser::getAcceptFunction(const char*& value) +{ + return [&value](const char* input) -> bool { + value = input; + return true; + }; +} + +AcceptFunction ArgParser::getAcceptFunction(std::string& value) +{ + return [&value](const char* input) -> bool { + value = input; + return true; + }; +} + +AcceptFunction ArgParser::getAcceptFunction(std::string_view& value) +{ + return [&value](const char* input) -> bool { + value = input; + return true; + }; +} + +AcceptFunction ArgParser::getAcceptFunction(int& value) +{ + return [&value](const char* input) -> bool { + const char* validate = input; + for (; *validate != '\0'; ++validate) { + // - [0-9] + if (*validate != 45 && (*validate < 48 || *validate > 57)) { + return false; + } + } + + try { + value = std::stoi(input); + return true; + } + catch (...) { + return false; + } + }; +} + +AcceptFunction ArgParser::getAcceptFunction(unsigned int& value) +{ + return [&value](const char* input) -> bool { + const char* validate = input; + for (; *validate != '\0'; ++validate) { + // [0-9] + if (*validate < 48 || *validate > 57) { + return false; + } + } + + unsigned long convert = 0; + try { + convert = std::stoul(input); + } + catch (...) { + return false; + } + + if (convert <= std::numeric_limits::max()) { + value = static_cast(convert); + return true; + } + + return false; + }; +} + +AcceptFunction ArgParser::getAcceptFunction(double& value) +{ + return [&value](const char* input) -> bool { + const char* validate = input; + for (; *validate != '\0'; ++validate) { + // . [0-9] + if (*validate != 46 && (*validate < 48 || *validate > 57)) { + return false; + } + } + + try { + value = std::stod(input); + return true; + } + catch (...) { + return false; + } + }; +} + +AcceptFunction ArgParser::getAcceptFunction(std::vector& value) +{ + return [&value](const char* input) -> bool { + value.push_back(input); + return true; + }; +} + +} // namespace Util diff --git a/src/util/argparser.h b/src/util/argparser.h new file mode 100644 index 0000000..1d45053 --- /dev/null +++ b/src/util/argparser.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 Riyyi + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include // size_t +#include // uint8_t +#include // function +#include +#include +#include + +namespace Util { + +using AcceptFunction = std::function; + +class ArgParser final { +public: + ArgParser(); + virtual ~ArgParser(); + + enum class Required : uint8_t { + No, + Yes, + Optional, + }; + + enum class Error : uint8_t { + None, + OptionInvalid, // For short options + OptionUnrecognized, // For long options + OptionDoesntAllowArgument, + OptionRequiresArgument, + OptionInvalidArgumentType, + ArgumentExtraOperand, + ArgumentRequired, + ArgumentInvalidType, + }; + + struct Option { + char shortName { 0 }; + const char* longName { nullptr }; + const char* argumentName { nullptr }; + const char* usageString { nullptr }; + const char* manString { nullptr }; + Required requiresArgument; + AcceptFunction acceptValue; + + Error error = Error::None; + }; + + struct Argument { + const char* name { nullptr }; + const char* usageString { nullptr }; + const char* manString { nullptr }; + size_t minValues { 0 }; + size_t maxValues { 1 }; + size_t addedValues { 0 }; + AcceptFunction acceptValue; + }; + + bool parse(int argc, const char* argv[]); + + void addOption(Option&& option); + void addOption(bool& value, char shortName, const char* longName, const char* usageString, const char* manString); + void addOption(const char*& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + void addOption(std::string& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + void addOption(std::string_view& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + void addOption(int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + void addOption(unsigned int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + void addOption(double& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + void addOption(std::vector& values, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + + void addArgument(Argument&& argument); + void addArgument(bool& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(const char*& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(std::string& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(std::string_view& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(int& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(unsigned int& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(double& value, const char* name, const char* usageString, const char* manString, Required required = Required::No); + void addArgument(std::vector& values, const char* name, const char* usageString, const char* manString, Required required = Required::No); + + void setErrorReporting(bool state) { m_errorReporting = state; } + void setExitOnFirstError(bool state) { m_exitOnFirstError = state; } + void setStopParsingOnFirstNonOption(bool state) { m_stopParsingOnFirstNonOption = state; } + +private: + void printError(char parameter, Error error); + void printError(const char* parameter, Error error, bool longName = true); + bool parseShortOption(std::string_view option, std::string_view next); + bool parseLongOption(std::string_view option, std::string_view next); + bool parseArgument(std::string_view argument); + + AcceptFunction getAcceptFunction(bool& value); + AcceptFunction getAcceptFunction(const char*& value); + AcceptFunction getAcceptFunction(std::string& value); + AcceptFunction getAcceptFunction(std::string_view& value); + AcceptFunction getAcceptFunction(int& value); + AcceptFunction getAcceptFunction(unsigned int& value); + AcceptFunction getAcceptFunction(double& value); + AcceptFunction getAcceptFunction(std::vector& value); + + bool m_errorReporting { true }; + bool m_exitOnFirstError { true }; + bool m_stopParsingOnFirstNonOption { false }; + + size_t m_optionIndex { 1 }; + size_t m_argumentIndex { 0 }; + bool m_nonOptionMode { false }; + + const char* m_name { nullptr }; + std::vector