commit 3bf3cfbc9cad944139145e1789534fdaf11f16b0 Author: Gwendolyn Date: Tue May 16 14:48:01 2023 +0200 something diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6e923d8 --- /dev/null +++ b/.clang-format @@ -0,0 +1,67 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: None +AlignOperands: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Always +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CompactNamespaces: false +ContinuationIndentWidth: 8 +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Right +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 0 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +SeparateDefinitionBlocks: Always \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9691ef5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/cmake-* +/.idea \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5f4a276 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.25) +project(argparser) + +set(CMAKE_CXX_STANDARD 23) + +find_program(BASH bash) + +add_custom_target(combined-header + COMMAND bash build.sh + BYPRODUCTS "${CMAKE_SOURCE_DIR}/dist/include/argparser/argparser.h" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + SOURCES src/_include_order.h src/argument.h src/basic-type.h src/builtin_parser.h src/defs.h + src/distinct-types.h src/errors.h src/list-type.h src/option.h src/optional-arg.h + src/parse-result.h src/parser.h src/parser_func.h src/repeat-arg.h src/repeat-flag.h + src/repeat-opt.h src/single-arg.h src/single-flag.h src/single-opt.h src/tuple-iteration.h + src/tuple-type.h src/type.h src/union-type.h + ) + +add_library(argparser INTERFACE) +target_include_directories(argparser INTERFACE dist/include) +add_dependencies(argparser combined-header) + + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# TODO: everything below this should only be for running cmake on this project, and not when it's included from somewhere else + + +include(FetchContent) +FETCHCONTENT_DECLARE( + gtest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG bc860af08783b8113005ca7697da5f5d49a8056f +) + +enable_testing() +FETCHCONTENT_MAKEAVAILABLE(gtest) + +include(GoogleTest) + +add_executable(ArgparserTest + tests/parser.cpp + tests/types.cpp + ) +target_link_libraries(ArgparserTest GTest::gtest_main) +target_link_libraries(ArgparserTest argparser) +gtest_discover_tests(ArgparserTest) +target_compile_options(ArgparserTest PRIVATE -Wall -Wextra -Wpedantic -Werror) \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..f234af5 --- /dev/null +++ b/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +rm -rf header-build +mkdir -p header-build/headers + +for header in src/*.h; do + grep -E '^#include <' "$header" >> "header-build/system-includes-1.h" + sed -e '/^#include "header-build/headers/$(basename "$header")" +done +sed -i '1s/^/\/*START*\/\n/' header-build/headers/_include_order.h +gcc -E -C -P "header-build/headers/_include_order.h" > header-build/combined-1.h +sed '0,/^\/\*START\*\/$/d' header-build/combined-1.h > header-build/combined-2.h +sort header-build/system-includes-1.h | uniq > header-build/system-includes-2.h +sed -r -e '/^namespace argparser \{/d' -e '/}\/\/ namespace argparser$/d' header-build/combined-2.h > header-build/combined-3.h +sed -r -e 's/^namespace argparser::(.*) \{/namespace \1 {/' -e 's/}\/\/ namespace argparser::(.*)$/}\/\/ namespace \1/' header-build/combined-3.h > header-build/combined-4.h +cat header-build/system-includes-2.h > header-build/combined.h +echo "namespace argparser {" >> header-build/combined.h +cat header-build/combined-4.h >> header-build/combined.h +echo "}// namespace argparser" >> header-build/combined.h +clang-tidy --fix-errors --quiet header-build/combined.h >/dev/null 2>&1 +clang-format -i -style=file:.clang-format header-build/combined.h +mkdir -p dist/include/argparser +cp header-build/combined.h dist/include/argparser/argparser.h +rm -rf header-build \ No newline at end of file diff --git a/dist/include/argparser/argparser.h b/dist/include/argparser/argparser.h new file mode 100644 index 0000000..cc97fa9 --- /dev/null +++ b/dist/include/argparser/argparser.h @@ -0,0 +1,1584 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace argparser { + namespace errors { + class runtime_error : public std::runtime_error { + public: + template + explicit runtime_error(Ts... args) : std::runtime_error(args...) {} + }; + + class logic_error : public std::logic_error { + public: + template + explicit logic_error(Ts... args) : std::logic_error(args...) {} + }; + + class missing_option_error : public runtime_error { + public: + explicit missing_option_error(std::string option_name) : runtime_error(std::format("missing required option {}", option_name)) {} + }; + + class missing_argument_error : public runtime_error { + public: + explicit missing_argument_error(std::string arg_name) : runtime_error(std::format("missing argument {}", arg_name)) {} + }; + + class unknown_option_error : public runtime_error { + public: + explicit unknown_option_error(std::string option_name) : runtime_error(std::format("unknown option {}", option_name)) {} + }; + + class wrong_option_count_error : public runtime_error { + public: + wrong_option_count_error(const std::string &option_name, std::optional min, std::optional max, unsigned int actual) + : runtime_error(make_message(option_name, min, max, actual)) {} + + private: + static std::string make_message(std::string option_name, std::optional min, std::optional max, unsigned int actual) { + if (min != std::nullopt && max != std::nullopt) { + return std::format("option {} was provided {} times, but is required at least {} times and at most {} times", option_name, actual, min.value(), max.value()); + } else if (min != std::nullopt) { + return std::format("option {} was provided {} times, but is required at least {} times", option_name, actual, min.value()); + } else { + return std::format("option {} was provided {} times, but is required at most {} times", option_name, actual, max.value()); + } + } + }; + + class missing_option_value_error : public runtime_error { + public: + explicit missing_option_value_error(std::string option_name) : runtime_error(std::format("missing value for option {}", option_name)) {} + }; + + class not_enough_arguments_error : public runtime_error { + public: + explicit not_enough_arguments_error(size_t min_required) : runtime_error(std::format("not enough arguments, need at least {}", min_required)) {} + }; + + class too_many_arguments_error : public runtime_error { + public: + explicit too_many_arguments_error() : runtime_error("too many arguments") {} + }; + + class invalid_option_value_error : public runtime_error { + public: + explicit invalid_option_value_error(std::string option_name) : runtime_error(std::format("invalid value for option {}", option_name)) {} + }; + + class unexpected_option_value_error : public runtime_error { + public: + explicit unexpected_option_value_error(std::string option_name) : runtime_error(std::format("unexpected value for option {}", option_name)) {} + }; + + class type_parsing_error : public runtime_error { + public: + type_parsing_error(std::string type_name, std::string input, size_t error_pos, std::string message) + : runtime_error(std::format("error parsing type {} at position {}: {}", type_name, error_pos, message)), + type_name(type_name), input(input), error_pos(error_pos) {} + + const std::string type_name; + const std::string input; + const int error_pos; + }; + + class ambiguous_parse_error : public type_parsing_error { + public: + explicit ambiguous_parse_error(std::string type_name, std::string input, size_t error_pos, std::vector possible_types) + : type_parsing_error(type_name, input, error_pos, make_message(possible_types)) {} + + private: + static std::string make_message(std::vector possible_types) { + std::string message = "ambiguity between "; + for (auto it = possible_types.begin(); it != possible_types.end(); it++) { + if (std::next(it) == possible_types.end()) { + message += " and " + *it; + } else { + message += ", " + *it; + } + } + return message; + } + }; + + class invalid_option_name_error : public logic_error { + public: + explicit invalid_option_name_error(std::string option_name) : logic_error(std::format("invalid option name {}", option_name)) {} + }; + + class duplicate_option_name : public logic_error { + public: + explicit duplicate_option_name(std::string option_name) : logic_error(std::format("option with name {} already exists", option_name)) {} + }; + + class duplicate_argument_name : public logic_error { + public: + explicit duplicate_argument_name(std::string arg_name) : logic_error(std::format("argument with name {} already exists", arg_name)) {} + }; + + class empty_enum_map_error : public logic_error { + public: + explicit empty_enum_map_error(std::string type_name) : logic_error(std::format("no enum values for type {}", type_name)) {} + }; + }// namespace errors + + namespace internal { + constexpr std::string_view whitespace = " \f\n\r\t\v"; + enum class parser_allow_undelimited { + None = 0, + Comma = 1, + Parenthesis = 2, + Brackets = 4, + Any = 7, + }; + + constexpr parser_allow_undelimited operator|(parser_allow_undelimited a, parser_allow_undelimited b) { + return static_cast(static_cast>(a) | static_cast>(b)); + } + + constexpr parser_allow_undelimited operator&(parser_allow_undelimited a, parser_allow_undelimited b) { + return static_cast(static_cast>(a) & static_cast>(b)); + } + enum class parse_opt { + None = 0, + BareString = 1, + SingleQuotedString = 2, + DoubleQuotedString = 4, + AnyString = 7, + TrimString = 8, + TrimBareString = 16, + }; + + constexpr parse_opt operator|(parse_opt a, parse_opt b) { + return static_cast(static_cast>(a) | static_cast>(b)); + } + + constexpr parse_opt operator&(parse_opt a, parse_opt b) { + return static_cast(static_cast>(a) & static_cast>(b)); + } + + template + constexpr bool enum_flag_contains(T a, T b) { + return static_cast>(a & b) != 0; + } + + template + using string_map = std::map>; + }// namespace internal + + class parse_result { + internal::string_map opts{}; + internal::string_map args{}; + std::vector remaining_args{}; + + public: + void set_opt(const std::string &name, std::any value) { + opts[name] = std::move(value); + } + + [[nodiscard]] std::any get_opt(const std::string &name) const { + if (opts.find(name) != opts.end()) { + return opts.at(name); + } else { + return {}; + } + } + + [[nodiscard]] bool has_opt(const std::string &name) const { + return opts.find(name) != opts.end(); + } + + void set_arg(const std::string &name, std::any value) { + args[name] = std::move(value); + } + + [[nodiscard]] std::any get_arg(const std::string &name) const { + if (args.find(name) != args.end()) { + return args.at(name); + } else { + return {}; + } + } + + [[nodiscard]] bool has_arg(const std::string &name) const { + auto argit = args.find(name); + if (argit == args.end()) + return false; + return argit != args.end() && argit->second.has_value(); + } + + void set_remaining(std::vector remaining) { + remaining_args = std::move(remaining); + } + + [[nodiscard]] std::vector remaining() const { + return remaining_args; + } + }; + + class type { + public: + virtual ~type() = default; + + [[nodiscard]] std::string get_name() const { + return name; + } + + protected: + explicit type(std::string name) : name(std::move(name)) {} + + std::string name; + }; + + using type_handle = std::shared_ptr; + + template + class type_impl : public type { + public: + virtual T parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited = internal::parser_allow_undelimited::None) = 0; + + protected: + explicit type_impl(std::string name) : type(std::move(name)) {} + }; + + template + using type_handle_impl = std::shared_ptr>; + + class arg { + public: + explicit arg(std::string name) : name(std::move(name)) {} + + virtual ~arg() = default; + + void parse(const std::string &input, parse_result &pr) const { + do_parse(input, pr); + } + + [[nodiscard]] bool has_parsed_enough(parse_result &pr) const { + return this->get_has_parsed_enough(pr); + } + + [[nodiscard]] bool can_parse_more(parse_result &pr) const { + return this->get_can_parse_more(pr); + } + + [[nodiscard]] std::string get_name() const { + return name; + } + + protected: + const std::string name; + + template + static T parse_single_value(std::string input, const type_handle_impl &type) { + const char *parse_end; + auto begin = &*input.begin(); + auto end = &*input.end(); + auto val = type->parse(begin, end, parse_end, internal::parser_allow_undelimited::Any); + if (parse_end != end) { + throw errors::type_parsing_error(type->get_name(), std::string(begin, end), parse_end - begin, "unexpected input"); + } + return val; + } + + private: + virtual void do_parse(std::string input, parse_result &pr) const = 0; + [[nodiscard]] virtual bool get_has_parsed_enough(parse_result &pr) const = 0; + [[nodiscard]] virtual bool get_can_parse_more(parse_result &pr) const = 0; + }; + + using arg_handle = std::shared_ptr; + + namespace internal { + template + using parser_func = std::function; + } + + template + class basic_type : public type_impl { + public: + basic_type(std::string name, internal::parser_func parser) : type_impl(std::move(name)), parser(std::move(parser)) {} + + T parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) override { + const char *n_parse_end; + auto val = parser(begin, end, n_parse_end, allow_undelimited); + parse_end = n_parse_end; + return val; + } + + private: + internal::parser_func parser; + }; + + namespace internal { + template + class automatic_parser { + public: + static parser_func make_parser(const std::string &name) { + return [name](const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited) { + T val; + auto pos = begin; + while (pos < end && isspace(*pos)) pos++; + auto res = std::from_chars(pos, end, val); + if (res.ec == std::errc{}) { + parse_end = res.ptr; + return val; + } else { + throw errors::type_parsing_error(name, std::string(begin, end), pos - begin, "invalid number"); + } + }; + } + }; + + constexpr parse_opt string_parse_opt_with_default(parse_opt opt) { + if ((opt & parse_opt::AnyString) == parse_opt::None) { + return opt | parse_opt::AnyString; + } else { + return opt; + } + } + + constexpr bool string_parser_enable_bare(parse_opt opt) { + return enum_flag_contains(string_parse_opt_with_default(opt), parse_opt::BareString); + } + + constexpr bool string_parser_enable_single_quoted(parse_opt opt) { + return enum_flag_contains(string_parse_opt_with_default(opt), parse_opt::SingleQuotedString); + } + + constexpr bool string_parser_enable_double_quoted(parse_opt opt) { + return enum_flag_contains(string_parse_opt_with_default(opt), parse_opt::DoubleQuotedString); + } + + template + class automatic_parser { + static std::string trim_string(const std::string &str) { + auto start = str.find_first_not_of(whitespace); + auto end = str.find_last_not_of(whitespace); + return str.substr(start == std::string::npos ? 0 : start, end == std::string::npos ? 0 : end); + } + + public: + static parser_func make_parser(const std::string &name) { + return [name](const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) { + auto str = std::string_view(begin, end); + if (str.length() == 0) { + if (parse_opt == parse_opt::None || enum_flag_contains(parse_opt, parse_opt::BareString)) { + parse_end = begin; + return std::string(""); + } else { + throw errors::type_parsing_error(name, std::string(""), 0, "unexpected empty input"); + } + } + if (str[0] == '\'' && string_parser_enable_single_quoted(parse_opt)) { + auto ss = std::stringstream(std::string(str)); + std::string val; + ss >> std::quoted(val, '\'', '\\'); + auto len = ss.tellg(); + parse_end = begin + len; + if (enum_flag_contains(parse_opt, parse_opt::TrimString)) { + return trim_string(val); + } else { + return val; + } + } + if (str[0] == '"' && string_parser_enable_double_quoted(parse_opt)) { + auto ss = std::stringstream(std::string(str)); + std::string val; + ss >> std::quoted(val, '"', '\\'); + auto len = ss.tellg(); + parse_end = begin + len; + if (enum_flag_contains(parse_opt, parse_opt::TrimString)) { + return trim_string(val); + } else { + return val; + } + } + if (string_parser_enable_bare(parse_opt)) { + std::string illegal_characters = "\"'"; + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Comma)) { + illegal_characters += ","; + } + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Parenthesis)) { + illegal_characters += "()"; + } + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Brackets)) { + illegal_characters += "[]"; + } + auto pos = str.find_first_of(illegal_characters); + if (pos == std::string_view::npos) { + parse_end = end; + } else { + parse_end = begin + pos; + } + std::string val{begin, parse_end}; + if (enum_flag_contains(parse_opt, parse_opt::TrimString) || enum_flag_contains(parse_opt, parse_opt::TrimBareString)) { + return trim_string(val); + } else { + return val; + } + } + throw errors::type_parsing_error(name, std::string(begin, end), 0, "failed to parse string"); + }; + } + }; + + template + parser_func make_enum_parser(const std::string &name, internal::string_map values) { + if (values.size() == 0) { + throw errors::empty_enum_map_error(name); + } + return [name, values](const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited) { + auto input = std::basic_string_view(begin, end); + auto start = input.find_first_not_of(whitespace); + if (start == std::string::npos) { + input = ""; + } else { + input = std::basic_string_view(begin + start, end); + } + T value; + size_t value_len = 0; + for (const auto &[str, val]: values) { + if (str.length() <= input.length()) { + if (input.starts_with(str)) { + if (str.length() > value_len) { + value = val; + value_len = str.length(); + } + } + } + } + if (value_len == 0) { + std::string expected_values = values.begin()->first; + for (auto it = std::next(values.begin()); it != values.end(); it++) { + if (std::next(it) != values.end()) { + expected_values += ", " + it->first; + } else { + expected_values += " or " + it->first; + } + } + throw errors::type_parsing_error(name, std::string(begin, end), 0, std::format("expected {}", expected_values)); + } else { + parse_end = begin + value_len; + return value; + } + }; + } + }// namespace internal + + namespace internal::distinct_types_impl { + template + struct UniqueTypes; + + template typename X, typename... Ts> + struct UniqueTypes> { + using type = X; + }; + + template typename X, typename... Ts, typename T, typename... Us> + struct UniqueTypes, T, Us...> { + using type = typename UniqueTypes< + typename std::conditional< + std::disjunction...>::value, + X, + X>::type, + Us...>::type; + }; + + template typename X, typename... Ts> + struct Distinct { + using type = typename UniqueTypes, Ts...>::type; + }; + }// namespace internal::distinct_types_impl + + namespace internal { + template + using distinct_types_variant = typename internal::distinct_types_impl::Distinct::type; + template + using distinct_types_tuple = typename internal::distinct_types_impl::Distinct::type; + template + using if_single_type = std::enable_if_t>::value == 1, T>; + template + using if_not_single_type = std::enable_if_t<(std::tuple_size>::value > 1), T>; + template + using single_type_conditional = std::conditional<(std::tuple_size>::value == 1), T, F>; + template + using single_type = if_single_type>::type, Ts...>; + }// namespace internal + + template + class list_type : public type_impl> { + public: + list_type(std::string name, type_handle_impl element_type) : type_impl>(std::move(name)), element_type(element_type) {} + + std::vector parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) override { + auto cur_pos = begin; + std::vector values{}; + bool is_delimited; + if (*cur_pos == '[') { + is_delimited = true; + cur_pos++; + } else { + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Comma)) { + throw errors::type_parsing_error(this->name, std::string(begin, end), cur_pos - begin, "expected '['"); + } else { + is_delimited = false; + } + } + if (cur_pos >= end) { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "unexpected end of input"); + } + internal::parser_allow_undelimited sub_parse_allow_undelimited = internal::parser_allow_undelimited::Parenthesis; + if (!is_delimited) { + sub_parse_allow_undelimited &allow_undelimited; + } + while (true) { + const char *this_parse_end; + auto val = element_type->parse(cur_pos, end, this_parse_end, sub_parse_allow_undelimited); + values.push_back(val); + cur_pos = this_parse_end; + while (std::isspace(*cur_pos) && cur_pos <= end) + cur_pos++; + if (cur_pos >= end) { + if (!is_delimited) { + break; + } else { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "unexpected end of input"); + } + } + if (*cur_pos == ',') { + cur_pos++; + auto s = std::string_view(cur_pos, end); + auto close_bracket_pos = s.find_first_of(']'); + if (s.find_first_not_of(internal::whitespace) == close_bracket_pos) { + break; + } + } else if (!is_delimited || *cur_pos == ']') { + break; + } else { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "expected , or ]"); + } + } + if (is_delimited) { + if (*cur_pos != ']') { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "unexpected end of input"); + } else { + cur_pos++; + } + } + parse_end = cur_pos; + return values; + } + + private: + type_handle_impl element_type; + }; + + class option { + public: + explicit option(std::string name) : name(std::move(name)) {} + + virtual ~option() = default; + + void parse(std::optional arg, std::any &val) { + return this->do_parse(std::move(arg), val); + } + + [[nodiscard]] std::string get_name() { + return name; + } + + [[nodiscard]] virtual bool consumes_value() const = 0; + virtual void validate(const parse_result &res) const = 0; + + protected: + const std::string name; + + private: + virtual void do_parse(std::optional arg, std::any &val) = 0; + }; + + using option_handle = std::shared_ptr