commit 8a1fb689bd01d775d2a0459ec4fae6d6c0bc3632 Author: Riyyi Date: Sat Sep 4 21:12:20 2021 +0200 Initial commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c8891ee --- /dev/null +++ b/.clang-format @@ -0,0 +1,38 @@ +# -*- yaml -*- + +--- +BasedOnStyle: WebKit +IndentWidth: 4 +--- +Language: Cpp + +AlignAfterOpenBracket: Align +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: true + +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortLambdasOnASingleLine: All + +AlwaysBreakTemplateDeclarations: Yes + +BraceWrapping: + AfterEnum: false + AfterFunction: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + SplitEmptyRecord: false +BreakBeforeBraces: Custom +BreakInheritanceList: BeforeComma + +SpaceAfterTemplateKeyword: false +SpaceInEmptyBlock: false +NamespaceIndentation: None +FixNamespaceComments: true +Standard: c++17 +TabWidth: 4 +UseTab: AlignWithSpaces +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe4e9e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Directories + +.cache/ +.clangd/ +build/ + +# Files diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4966393 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,74 @@ +# User config between these lines +# ------------------------------------------ + +# Set project name +set(PROJECT "stowage") +# Set debugging, ON/OFF +set(DEBUG "ON") + +# ------------------------------------------ + +# Add 'make run' target +add_custom_target(run + COMMAND ${PROJECT} +) + +# ------------------------------------------ + +cmake_minimum_required(VERSION 3.16) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Check if the build should include debugging symbols +option(DEBUG "" ${DEBUG}) +if(DEBUG) + # cmake -DDEBUG=on .. && make + message("--- Debug ---") + set(CMAKE_BUILD_TYPE "Debug") + + # -Og = Optimizations that do not interfere with debugging + # -Wall = All warnings about contructions that are easily avoidable + # -Wextra = Extra warning flags not covered by -Wall + # -g = Produce debugging information in OS's native format + # -pg = Generate profile information for analysis with gprof + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -Wall -Wextra -g -pg") + # gprof gmon.out > profile-data.txt +else() + # cmake -DDEBUG=off .. && make + message("--- Release ---") + set(CMAKE_BUILD_TYPE "Release") + + # -O3 = Optimizations that increases compilation time and performance + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") +endif() + +# Include all headers +include_directories( + "src" + "test" +) + +# Define source files +file(GLOB_RECURSE PROJECT_SOURCES "src/*.cpp") +set(PROJECT_SOURCES ${PROJECT_SOURCES}) + +# Define test source files +file(GLOB_RECURSE TEST_SOURCES "test/*.cpp") +file(GLOB_RECURSE MAIN_SOURCES "src/*/*.cpp") +set(TEST_SOURCES ${TEST_SOURCES} ${MAIN_SOURCES}) + +# ------------------------------------------ + +project(${PROJECT}) +set(CMAKE_CXX_STANDARD 17) + +add_executable(${PROJECT} ${PROJECT_SOURCES}) +target_link_libraries(${PROJECT}) + +# ------------------------------------------ + +project(test) +set(CMAKE_CXX_STANDARD 17) + +add_executable(test ${TEST_SOURCES}) +target_link_libraries(test) diff --git a/README.org b/README.org new file mode 100644 index 0000000..a2b7631 --- /dev/null +++ b/README.org @@ -0,0 +1,74 @@ +* Stowage + +Needed information for generation: +- short option +- long option +- short description (for usage ouput) +- long description (for manpage output) + +* Option Parsing + +Parsed from left to right. + +** Types + +- Short option +- Long option +- Argument to option +- Non-option parameter (default argument) + +*** Short Option + +'-' followed by short option character. +If option has required argument, directly after OR separated by white-space. +Optional argument must be directly after the option character. + +Possible to write several short options after one '-', +if all (except last) do not have *required* or *optional* arguments. + +*** Long Option + +'--' followed by long option name. +If option has a required argument, directly after, separated by '=' OR white-space. +Optional argument must be directly after the option, separated by '='. + +*** Non-option Parameter + +Each parameter not starting with '-' and not a required argument of a previous option, +is a non-option parameter. + +Each parameter after a '--' parameter is always interpreted as a non-option parameter. + +* Examples + +#+BEGIN_SRC shell-script +./stowage -Fals +./stowage -F -a -l -s + +./stowage --file --add --pull --push + +./stowage -r filename +./stowage -rfilename +./stowage --remove=filename +./stowage --remove filename + +./stowage --remove filename other stuff +./stowage --remove filename -- other stuff +#+END_SRC + +** Multiple of the same options + +#+BEGIN_SRC shell-script +./stowage -e pattern1 -epattern2 --regexp=pattern3 --regexp pattern4 +#+END_SRC + +* TODO + +- after first non option, go into no-option mode +- support '--' to go into no-option mode +- add multi-option support, vectors! +- support argument parsing + storing +- add addOption overloads +- generate usage string +- generate man page string +- parse() function to return bool true if any error has occured diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..25eb4b2 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +build/compile_commands.json \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6efc5e6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,39 @@ +// #include // size_t +// #include // maloc, free +#include + +#include "util/argparser.h" + +// void* operator new(size_t size) +// { +// std::cout << "Allocating '" << size << "' bytes" << std::endl; +// return std::malloc(size); +// } + +// void operator delete(void* pointer, size_t size) +// { +// std::cout << "Freeing '" << size << "' bytes" << std::endl; +// free(pointer); +// } + +int main(int argc, const char* argv[]) +{ + bool pattern = false; + std::string stringArg1 = "default value"; + std::string stringArg2 = "nothing"; + + Util::ArgParser parser; + // parser.setExitOnFirstError(false); + // parser.setErrorMessages(false); + + parser.addOption(pattern, 'e', "regexp", "search pattern", "Use ${U}PATTERNS${N} as the patterns."); + parser.addOption(stringArg1, 'a', "arg1", "test argument", "Test argument manpage description.", "TEST", Util::ArgParser::Required::Yes); + parser.addOption(stringArg2, 'b', "arg2", "optional argument", "Option with optional argument", "TEST", Util::ArgParser::Required::Optional); + parser.parse(argc, argv); + + printf(" Pattern: {%d}\n", pattern); + printf(" Arg1: {%s}\n", stringArg1.data()); + printf(" Arg2: {%s}\n", stringArg2.data()); + + return 0; +} diff --git a/src/util/argparser.cpp b/src/util/argparser.cpp new file mode 100644 index 0000000..283f2bf --- /dev/null +++ b/src/util/argparser.cpp @@ -0,0 +1,281 @@ +#include // std::find_if +#include // printf +#include + +#include "util/argparser.h" + +namespace Util { + +ArgParser::ArgParser() +{ +} + +ArgParser::~ArgParser() +{ +} + +void ArgParser::printOptionError(char name, Error error) +{ + char tmp[] { name, '\0' }; + printOptionError(tmp, error, false); +} + +void ArgParser::printOptionError(const char* name, Error error, bool longName) +{ + if (!m_errorMessages) { + return; + } + + if (error == Error::InvalidOption) { + printf("%s: invalid option -- '%s'\n", m_name, name); + } + else if (error == Error::UnrecognizedOption) { + printf("%s: unrecognized option -- '%s'\n", m_name, name); + } + else if (error == Error::DoesntAllowArgument) { + printf("%s: option '--%s' doesn't allow an argument\n", m_name, name); + } + else if (error == Error::RequiresArgument) { + if (longName) { + printf("%s: option '--%s' requires an argument", m_name, name); + } + else { + printf("%s: option requires an argument -- '%s'\n", m_name, name); + } + } + + // 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; + + printf("Parsing short option: '%s'\n", option.data()); + + char c; + std::string_view value; + for (std::string_view::size_type i = 0; i < option.size(); ++i) { + c = option.at(i); + printf("short '%c'\n", c); + + 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()) { + printOptionError(c, Error::InvalidOption); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + + if (foundOption->requiresArgument == Required::No) { + // FIXME: Figure out why providing a nullptr breaks the lambda here. + foundOption->acceptValue(""); + } + else if (foundOption->requiresArgument == Required::Yes) { + value = option.substr(i + 1); + if (value.empty() && next.empty()) { + foundOption->error = Error::RequiresArgument; + printOptionError(c, Error::RequiresArgument); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + else if (!value.empty()) { + foundOption->acceptValue(value.data()); + } + else if (next[0] == '-') { + foundOption->error = Error::RequiresArgument; + printOptionError(c, Error::RequiresArgument); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + else { + foundOption->acceptValue(next.data()); + m_optionIndex++; + } + + break; + } + else if (foundOption->requiresArgument == Required::Optional) { + value = option.substr(i + 1); + if (!value.empty()) { + foundOption->acceptValue(value.data()); + 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) +{ + bool result = true; + + std::string_view name = option.substr(0, option.find_first_of('=')); + std::string_view value = option.substr(option.find_first_of('=') + 1); + + bool argumentProvided = true; + if (name.compare(value) == 0 && option.find('=') == std::string_view::npos) { + argumentProvided = false; + } + + printf("Parsing long option: '%s' with value '%s'\n", name.data(), argumentProvided ? value.data() : ""); + + auto foundOption = std::find_if(m_options.begin(), m_options.end(), [&name](Option& it) -> bool { + return it.longName == name; + }); + + if (foundOption == m_options.cend()) { + foundOption->error = Error::UnrecognizedOption; + printOptionError(name.data(), Error::UnrecognizedOption); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + + if (argumentProvided) { + if (foundOption->requiresArgument == Required::No) { + foundOption->error = Error::DoesntAllowArgument; + printOptionError(name.data(), Error::DoesntAllowArgument); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + else if (foundOption->requiresArgument == Required::Yes) { + foundOption->acceptValue(value.data()); + } + else if (foundOption->requiresArgument == Required::Optional) { + foundOption->acceptValue(value.data()); + } + } + else if (!next.empty() && foundOption->requiresArgument == Required::Yes) { + if (next[0] == '-') { + foundOption->error = Error::RequiresArgument; + printOptionError(name.data(), Error::RequiresArgument); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + else { + foundOption->acceptValue(next.data()); + m_optionIndex++; + } + } + else if (foundOption->requiresArgument == Required::Yes) { + foundOption->error = Error::RequiresArgument; + printOptionError(name.data(), Error::RequiresArgument); + + result = false; + if (m_exitOnFirstError) { + return result; + } + } + else { + // FIXME: Figure out why providing a nullptr breaks the lambda here. + foundOption->acceptValue(""); + } + + return result; +} + +bool ArgParser::parse(int argc, const char* argv[]) +{ + // Get program name + m_name = argv[0] + std::string_view(argv[0]).find_last_of('/') + 1; + + printf("name: %s\n", m_name); + + std::string_view argument; + std::string_view next; + for (; m_optionIndex < argc; ++m_optionIndex) { + printf("argv[%d]: %s\n", m_optionIndex, argv[m_optionIndex]); + + argument = argv[m_optionIndex]; + if (m_optionIndex + 1 < argc && argv[m_optionIndex + 1][0] != '-') { + next = argv[m_optionIndex + 1]; + } + else { + next = {}; + } + + // Long Option + if (argument[0] == '-' && argument[1] == '-') { + argument = argument.substr(argument.find_first_not_of('-')); + parseLongOption(argument, next); + } + // Short Option + else if (argument[0] == '-') { + argument = argument.substr(argument.find_first_not_of('-')); + parseShortOption(argument, next); + } + // Argument + else { + printf("-> argu: '%s'", argument.data()); + } + } + + return true; +} + +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) +{ + Option option { + shortName, + longName, + nullptr, + usageString, + manString, + Required::No, + [&value](const char*) -> bool { + value = true; + return true; + } + }; + addOption(std::move(option)); +} + +void ArgParser::addOption(std::string& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument) +{ + Option option { + shortName, + longName, + argumentName, + usageString, + manString, + requiresArgument, + [&value](const char* a) -> bool { + value = a; + return true; + } + }; + addOption(std::move(option)); +} + +} // namespace Util diff --git a/src/util/argparser.h b/src/util/argparser.h new file mode 100644 index 0000000..03480f3 --- /dev/null +++ b/src/util/argparser.h @@ -0,0 +1,75 @@ +#ifndef ARG_PARSER_H +#define ARG_PARSER_H + +#include +#include +#include +#include + +namespace Util { + +class ArgParser final { +public: + ArgParser(); + virtual ~ArgParser(); + + enum class Required { + No, + Yes, + Optional, + }; + + enum class Error { + None, + InvalidOption, // For short options + UnrecognizedOption, // For long options + DoesntAllowArgument, + RequiresArgument, + }; + + struct Option { + char shortName { 0 }; + const char* longName { nullptr }; + const char* argumentName { nullptr }; + const char* usageString { nullptr }; + const char* manString { nullptr }; + Required requiresArgument; + std::function acceptValue; + + Error error = Error::None; + }; + + struct Argument { + }; + + 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(std::string& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No); + + void setOptionIndex(int index) { m_optionIndex = index; } + void setExitOnFirstError(bool state) { m_exitOnFirstError = state; } + void setErrorMessages(bool state) { m_errorMessages = state; } + void setNonOptionAfterFirst(bool state) { m_nonOptionAfterFirst = state; } + +private: + void printOptionError(char name, Error error); + void printOptionError(const char* name, 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); + + int m_optionIndex { 1 }; + bool m_exitOnFirstError { true }; + bool m_errorMessages { true }; + // TODO: Implement this, maybe combine with error messages flag, enum class? or bitfield + bool m_nonOptionAfterFirst { false }; + + const char* m_name; + std::vector