From d5b99ef407c6f7b209a6078e03aa1b625a77dac5 Mon Sep 17 00:00:00 2001 From: killua Date: Mon, 16 Mar 2026 20:53:00 +0400 Subject: [PATCH 1/2] feat: improve help text for better readability, less duplication on the conventions, better syntax information. --- README.md | 123 ++++++++++++++++++ src/headers/conventions/base_convention.hpp | 5 + .../gnu_argument_convention.hpp | 8 ++ .../windows_argument_convention.hpp | 8 ++ src/headers/parser/argument_parser.hpp | 2 +- src/headers/parser/parser_v2.hpp | 17 ++- src/main.cpp | 4 +- .../gnu_argument_convention.cpp | 50 +++++++ .../windows_argument_convention.cpp | 59 +++++++++ src/source/parser/argument_parser.cpp | 80 +++++++++--- 10 files changed, 332 insertions(+), 24 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e21e8d --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# argument-parser + +A lightweight, modern, expressively typed, and highly customizable C++17 argument parser library. + +## Features + +- **Type-safe Argument Extraction**: Use type traits to automatically parse fundamental types and custom structures (e.g. `std::vector`, `std::regex`, `Point`). +- **Support for Multiple Parsing Conventions**: Pluggable convention system out of the box, offering GNU-style (`-a`, `--arg`), GNU-equal-style (`--arg=value`), Windows-style (`/arg`), and Windows-equal-style (`/arg:value`). +- **Automated Help Text Formatting**: Call `parser.display_help(conventions)` to easily generate beautifully formatted usage instructions. +- **Cross-Platform Native Parsers**: Dedicated parsers that automatically fetch command-line arguments using OS-specific APIs (`windows_parser`, `linux_parser`, `macos_parser`), so you don't need to manually pass `argc` and `argv` on most platforms. +- **Fluid setup**: Enjoy fluid setup routines with maps and initializer lists. + +### Important Note: +V1 is deprecated and is mainly kept as a base implementation for the V2. You should use V2 for your projects. If any features are missing compared to V1, please let me know so I can introduce them! + +## Requirements + +- C++17 or later +- CMake 3.15 or later + +## Quick Start + +### 1. Create your Parser and Define Arguments + +```cpp +#include +#include +#include +#include // Provides the native parser for your compiling platform + +int main() { + using namespace argument_parser::v2::flags; + + // Automatically uses the platform-native parser! + // It will fetch arguments directly from OS APIs (e.g., GetCommandLineW on Windows) + argument_parser::v2::parser parser; + + // A flag with an action + parser.add_argument({ + {ShortArgument, "e"}, + {LongArgument, "echo"}, + {Action, argument_parser::helpers::make_parametered_action( + [](std::string const &text) { std::cout << text << std::endl; } + )}, + {HelpText, "echoes given variable"} + }); + + // A flag that just stores the value to extract later + parser.add_argument({ + {ShortArgument, "g"}, + {LongArgument, "grep"}, + {HelpText, "Grep pattern"} + }); + + // A required flag + parser.add_argument({ + {LongArgument, "file"}, + {Required, true}, + {HelpText, "File to grep"} + }); + + // Run action callback on complete + parser.on_complete([](argument_parser::base_parser const &p) { + auto filename = p.get_optional("file"); + auto pattern = p.get_optional("grep"); + + if (filename && pattern) { + std::cout << "Grepping " << filename.value() << " with pattern." << std::endl; + } + }); + + // Register Conventions + const std::initializer_list conventions = { + &argument_parser::conventions::gnu_argument_convention, + &argument_parser::conventions::windows_argument_convention + }; + + // Execute logic! + parser.handle_arguments(conventions); + + return 0; +} +``` + +### 2. Custom Type Parsing + +You can natively parse your custom structs, objects, or arrays by specializing `argument_parser::parsing_traits::parser_trait`. + +```cpp +struct Point { + int x, y; +}; + +template <> struct argument_parser::parsing_traits::parser_trait { + static Point parse(const std::string &input) { + auto comma_pos = input.find(','); + int x = std::stoi(input.substr(0, comma_pos)); + int y = std::stoi(input.substr(comma_pos + 1)); + return {x, y}; + } +}; + +// Now you can directly use your type: +// parser.add_argument({ {LongArgument, "point"} }); +// auto point = parser.get_optional("point"); +``` + +## CMake Integration + +The library can be installed globally via CMake or incorporated into your project. + +```cmake +add_subdirectory(argument-parser) +target_link_libraries(your_target PRIVATE argument_parser) +``` + +## Building & Installing + +```bash +mkdir build && cd build +cmake .. +cmake --build . +``` diff --git a/src/headers/conventions/base_convention.hpp b/src/headers/conventions/base_convention.hpp index 96dc59e..b5cae2c 100644 --- a/src/headers/conventions/base_convention.hpp +++ b/src/headers/conventions/base_convention.hpp @@ -1,11 +1,13 @@ #pragma once #include #include +#include #ifndef BASE_CONVENTION_HPP #define BASE_CONVENTION_HPP namespace argument_parser::conventions { + enum class convention_features { ALLOW_SHORT_TO_LONG_FALLBACK, ALLOW_LONG_TO_SHORT_FALLBACK }; enum class argument_type { SHORT, LONG, POSITIONAL, INTERCHANGABLE, ERROR }; using parsed_argument = std::pair; @@ -18,6 +20,9 @@ namespace argument_parser::conventions { virtual std::string name() const = 0; virtual std::string short_prec() const = 0; virtual std::string long_prec() const = 0; + virtual std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const = 0; + virtual std::vector get_features() const = 0; protected: base_convention() = default; diff --git a/src/headers/conventions/implementations/gnu_argument_convention.hpp b/src/headers/conventions/implementations/gnu_argument_convention.hpp index 986e6d0..8867eb4 100644 --- a/src/headers/conventions/implementations/gnu_argument_convention.hpp +++ b/src/headers/conventions/implementations/gnu_argument_convention.hpp @@ -14,6 +14,10 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; + std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const override; + std::vector get_features() const override; + static gnu_argument_convention instance; private: @@ -28,6 +32,10 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; + std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const override; + std::vector get_features() const override; + static gnu_equal_argument_convention instance; private: diff --git a/src/headers/conventions/implementations/windows_argument_convention.hpp b/src/headers/conventions/implementations/windows_argument_convention.hpp index 06e7a5d..4cfbdd3 100644 --- a/src/headers/conventions/implementations/windows_argument_convention.hpp +++ b/src/headers/conventions/implementations/windows_argument_convention.hpp @@ -18,6 +18,10 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; + std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const override; + std::vector get_features() const override; + static windows_argument_convention instance; private: @@ -33,6 +37,10 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; + std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const override; + std::vector get_features() const override; + static windows_kv_argument_convention instance; private: diff --git a/src/headers/parser/argument_parser.hpp b/src/headers/parser/argument_parser.hpp index 799a088..da89814 100644 --- a/src/headers/parser/argument_parser.hpp +++ b/src/headers/parser/argument_parser.hpp @@ -256,7 +256,7 @@ namespace argument_parser { std::optional &found_help); void invoke_arguments(std::unordered_map const &values_for_arguments, - std::vector> const &found_arguments, + std::vector> &found_arguments, std::optional const &found_help); void enforce_creation_thread(); diff --git a/src/headers/parser/parser_v2.hpp b/src/headers/parser/parser_v2.hpp index 0298805..971f7c0 100644 --- a/src/headers/parser/parser_v2.hpp +++ b/src/headers/parser/parser_v2.hpp @@ -120,10 +120,10 @@ namespace argument_parser::v2 { found_params[extended_add_argument_flags::LongArgument] = true; long_arg = get_or_throw(argument_pairs.at(add_argument_flags::LongArgument), "long"); if (short_arg.empty()) - short_arg = long_arg; + short_arg = "-"; } else { if (!short_arg.empty()) - long_arg = short_arg; + long_arg = "-"; } if (argument_pairs.find(add_argument_flags::Action) != argument_pairs.end()) { @@ -133,7 +133,18 @@ namespace argument_parser::v2 { if (argument_pairs.find(add_argument_flags::HelpText) != argument_pairs.end()) { help_text = get_or_throw(argument_pairs.at(add_argument_flags::HelpText), "help"); } else { - help_text = short_arg + ", " + long_arg; + help_text = ""; + if (short_arg != "-") { + help_text += short_arg; + } + + if (long_arg != "-") { + if (!help_text.empty()) { + help_text += ", "; + } + + help_text += long_arg; + } } if (argument_pairs.find(add_argument_flags::Required) != argument_pairs.end() && diff --git a/src/main.cpp b/src/main.cpp index 67727e7..b798957 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -168,8 +167,9 @@ int v2Examples() { } int main() { + return v2Examples(); + try { - return v2Examples(); } catch (std::exception const &e) { std::cerr << "Error: " << e.what() << std::endl; return -1; diff --git a/src/source/conventions/implementations/gnu_argument_convention.cpp b/src/source/conventions/implementations/gnu_argument_convention.cpp index 85baa88..a0b0e93 100644 --- a/src/source/conventions/implementations/gnu_argument_convention.cpp +++ b/src/source/conventions/implementations/gnu_argument_convention.cpp @@ -35,6 +35,31 @@ namespace argument_parser::conventions::implementations { std::string gnu_argument_convention::long_prec() const { return "--"; } + + std::vector gnu_argument_convention::get_features() const { + return {}; // no fallback allowed + } + + std::string gnu_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const { + std::string res = ""; + if (short_arg != "-" && short_arg != "") { + res += short_prec() + short_arg; + if (requires_value) { + res += " "; + } + } + if (long_arg != "-" && long_arg != "") { + if (!res.empty()) { + res += ", "; + } + res += long_prec() + long_arg; + if (requires_value) { + res += " "; + } + } + return res; + } } // namespace argument_parser::conventions::implementations namespace argument_parser::conventions::implementations { @@ -72,4 +97,29 @@ namespace argument_parser::conventions::implementations { return "--"; } + std::string gnu_equal_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const { + std::string res = ""; + if (short_arg != "-" && short_arg != "") { + res += short_prec() + short_arg; + if (requires_value) { + res += "="; + } + } + if (long_arg != "-" && long_arg != "") { + if (!res.empty()) { + res += ", "; + } + res += long_prec() + long_arg; + if (requires_value) { + res += "="; + } + } + + return res; + } + + std::vector gnu_equal_argument_convention::get_features() const { + return {}; // no fallback allowed + } } // namespace argument_parser::conventions::implementations \ No newline at end of file diff --git a/src/source/conventions/implementations/windows_argument_convention.cpp b/src/source/conventions/implementations/windows_argument_convention.cpp index 91c9771..59a799b 100644 --- a/src/source/conventions/implementations/windows_argument_convention.cpp +++ b/src/source/conventions/implementations/windows_argument_convention.cpp @@ -1,4 +1,5 @@ #include "windows_argument_convention.hpp" +#include "base_convention.hpp" #include namespace argument_parser::conventions::implementations { @@ -49,6 +50,33 @@ namespace argument_parser::conventions::implementations { return "/"; } + std::string windows_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, + bool requires_value) const { + std::string res = ""; + if (short_arg != "-" && short_arg != "") { + res += short_prec() + short_arg; + if (requires_value) { + res += " "; + } + } + + if (long_arg != "-" && long_arg != "") { + if (!res.empty()) { + res += ", "; + } + res += long_prec() + long_arg; + if (requires_value) { + res += " "; + } + } + + return res; + } + + std::vector windows_argument_convention::get_features() const { + return {convention_features::ALLOW_LONG_TO_SHORT_FALLBACK, + convention_features::ALLOW_SHORT_TO_LONG_FALLBACK}; // interchangable + } } // namespace argument_parser::conventions::implementations namespace argument_parser::conventions::implementations { @@ -101,4 +129,35 @@ namespace argument_parser::conventions::implementations { std::string windows_kv_argument_convention::long_prec() const { return "/"; } + + std::string windows_kv_argument_convention::make_help_text(std::string const &short_arg, + std::string const &long_arg, bool requires_value) const { + std::string res = ""; + if (short_arg != "-" && short_arg != "") { + res += short_prec() + short_arg; + if (requires_value) { + res += "="; + res += ", " + short_prec() + short_arg; + res += ":"; + } + } + + if (long_arg != "-" && long_arg != "") { + if (!res.empty()) { + res += ", "; + } + res += long_prec() + long_arg; + if (requires_value) { + res += "=" + ", " + + long_prec() + long_arg + ":"; + } + } + return res; + } + + std::vector windows_kv_argument_convention::get_features() const { + return {convention_features::ALLOW_LONG_TO_SHORT_FALLBACK, + convention_features::ALLOW_SHORT_TO_LONG_FALLBACK}; // interchangable + } } // namespace argument_parser::conventions::implementations \ No newline at end of file diff --git a/src/source/parser/argument_parser.cpp b/src/source/parser/argument_parser.cpp index a14ed97..fee3e31 100644 --- a/src/source/parser/argument_parser.cpp +++ b/src/source/parser/argument_parser.cpp @@ -4,8 +4,10 @@ #include #include #include +#include #include #include +#include #include class deferred_exec { @@ -85,11 +87,19 @@ namespace argument_parser { ss << "Usage: " << program_name << " [OPTIONS]...\n"; for (auto const &[id, arg] : argument_map) { - auto short_arg = reverse_short_arguments.at(id); - auto long_arg = reverse_long_arguments.at(id); + auto short_arg = + reverse_short_arguments.find(id) != reverse_short_arguments.end() ? reverse_short_arguments.at(id) : ""; + auto long_arg = + reverse_long_arguments.find(id) != reverse_long_arguments.end() ? reverse_long_arguments.at(id) : ""; + ss << "\t"; + std::unordered_set hasOnce; for (auto const &convention : convention_types) { - ss << convention->short_prec() << short_arg << ", " << convention->long_prec() << long_arg << "\t"; + auto generatedHelpText = convention->make_help_text(short_arg, long_arg, arg.expects_parameter()); + if (hasOnce.find(generatedHelpText) == hasOnce.end()) { + ss << generatedHelpText << "\t"; + hasOnce.insert(generatedHelpText); + } } ss << arg.help_text << "\n"; } @@ -143,20 +153,19 @@ namespace argument_parser { if (extracted.second == "h" || extracted.second == "help") { found_help = corresponding_argument; - continue; + return true; } found_arguments.emplace_back(extracted.second, corresponding_argument); if (corresponding_argument.expects_parameter()) { if (convention_type->requires_next_token() && (it + 1) == parsed_arguments.end()) { - throw std::runtime_error("expected value for argument " + extracted.second); + throw std::runtime_error("Expected value for argument " + extracted.second); } values_for_arguments[extracted.second] = convention_type->requires_next_token() ? *(++it) : convention_type->extract_value(*it); } - corresponding_argument.set_invoked(true); return true; } catch (const std::runtime_error &e) { error_stream << "Convention \"" << convention_type->name() << "\" failed with: " << e.what() << "\n"; @@ -183,7 +192,7 @@ namespace argument_parser { } void base_parser::invoke_arguments(std::unordered_map const &values_for_arguments, - std::vector> const &found_arguments, + std::vector> &found_arguments, std::optional const &found_help) { if (found_help) { @@ -192,13 +201,14 @@ namespace argument_parser { } std::stringstream error_stream; - for (auto const &[key, value] : found_arguments) { + for (auto &[key, value] : found_arguments) { try { if (value.expects_parameter()) { value.action->invoke_with_parameter(values_for_arguments.at(key)); } else { value.action->invoke(); } + value.set_invoked(true); } catch (const std::runtime_error &e) { error_stream << "Argument " << key << " failed with: " << e.what() << "\n"; } @@ -255,26 +265,60 @@ namespace argument_parser { void base_parser::place_argument(int id, argument const &arg, std::string const &short_arg, std::string const &long_arg) { argument_map[id] = arg; - short_arguments[short_arg] = id; - reverse_short_arguments[id] = short_arg; - long_arguments[long_arg] = id; - reverse_long_arguments[id] = long_arg; + if (short_arg != "-") { + short_arguments[short_arg] = id; + reverse_short_arguments[id] = short_arg; + } + if (long_arg != "-") { + long_arguments[long_arg] = id; + reverse_long_arguments[id] = long_arg; + } + } + + std::string get_one_name(std::string const &short_name, std::string const &long_name) { + std::string res{}; + if (short_name != "-") { + res += short_name; + } + + if (long_name != "-") { + if (!res.empty()) { + res += ", "; + } + + res += long_name; + } + return res; } void base_parser::check_for_required_arguments( std::initializer_list convention_types) { - std::vector> required_args; + std::vector> required_args; for (auto const &[key, arg] : argument_map) { if (arg.is_required() && !arg.is_invoked()) { - required_args.emplace_back>( - {reverse_short_arguments[key], reverse_long_arguments[key]}); + auto short_arg = reverse_short_arguments.find(key) != reverse_short_arguments.end() + ? reverse_short_arguments.at(key) + : "-"; + auto long_arg = reverse_long_arguments.find(key) != reverse_long_arguments.end() + ? reverse_long_arguments.at(key) + : "-"; + + required_args.emplace_back>( + {short_arg, long_arg, arg.expects_parameter()}); } } if (!required_args.empty()) { - std::cerr << "These arguments were expected but not provided: "; - for (auto const &[s, l] : required_args) { - std::cerr << "[-" << s << ", --" << l << "] "; + std::cerr << "These arguments were expected but not provided: \n"; + for (auto const &[s, l, p] : required_args) { + std::cerr << "\t" << get_one_name(s, l) << ": must be provided as one of ["; + for (auto it = convention_types.begin(); it != convention_types.end(); ++it) { + std::cerr << (*it)->make_help_text(s, l, p); + if (it + 1 != convention_types.end()) { + std::cerr << ", "; + } + } + std::cerr << "]\n"; } std::cerr << "\n"; display_help(convention_types); From 8e502bcb8bc7b8e5b3be7939a15da75454f1dbc2 Mon Sep 17 00:00:00 2001 From: killua Date: Mon, 16 Mar 2026 21:50:05 +0400 Subject: [PATCH 2/2] feat: improve the readability of the generated help text. add type hints for the generation of the help text when description is not given for the variable. use sfinae for the check so that it compiles if not given. --- src/headers/conventions/base_convention.hpp | 2 +- .../gnu_argument_convention.hpp | 4 +- .../windows_argument_convention.hpp | 4 +- src/headers/parser/parser_v2.hpp | 73 +++++++++++++++---- src/headers/parser/parsing_traits/traits.hpp | 14 ++++ src/main.cpp | 19 ++++- .../gnu_argument_convention.cpp | 53 +++++++------- .../windows_argument_convention.cpp | 41 +++++------ src/source/parser/argument_parser.cpp | 59 +++++++++++++-- 9 files changed, 190 insertions(+), 79 deletions(-) diff --git a/src/headers/conventions/base_convention.hpp b/src/headers/conventions/base_convention.hpp index b5cae2c..3c9463c 100644 --- a/src/headers/conventions/base_convention.hpp +++ b/src/headers/conventions/base_convention.hpp @@ -20,7 +20,7 @@ namespace argument_parser::conventions { virtual std::string name() const = 0; virtual std::string short_prec() const = 0; virtual std::string long_prec() const = 0; - virtual std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + virtual std::pair make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const = 0; virtual std::vector get_features() const = 0; diff --git a/src/headers/conventions/implementations/gnu_argument_convention.hpp b/src/headers/conventions/implementations/gnu_argument_convention.hpp index 8867eb4..b79c689 100644 --- a/src/headers/conventions/implementations/gnu_argument_convention.hpp +++ b/src/headers/conventions/implementations/gnu_argument_convention.hpp @@ -14,7 +14,7 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; - std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + std::pair make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const override; std::vector get_features() const override; @@ -32,7 +32,7 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; - std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + std::pair make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const override; std::vector get_features() const override; diff --git a/src/headers/conventions/implementations/windows_argument_convention.hpp b/src/headers/conventions/implementations/windows_argument_convention.hpp index 4cfbdd3..5a6de0b 100644 --- a/src/headers/conventions/implementations/windows_argument_convention.hpp +++ b/src/headers/conventions/implementations/windows_argument_convention.hpp @@ -18,7 +18,7 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; - std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + std::pair make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const override; std::vector get_features() const override; @@ -37,7 +37,7 @@ namespace argument_parser::conventions::implementations { std::string name() const override; std::string short_prec() const override; std::string long_prec() const override; - std::string make_help_text(std::string const &short_arg, std::string const &long_arg, + std::pair make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const override; std::vector get_features() const override; diff --git a/src/headers/parser/parser_v2.hpp b/src/headers/parser/parser_v2.hpp index 971f7c0..a67209c 100644 --- a/src/headers/parser/parser_v2.hpp +++ b/src/headers/parser/parser_v2.hpp @@ -1,4 +1,5 @@ #pragma once +#include "traits.hpp" #include #include #include @@ -103,6 +104,30 @@ namespace argument_parser::v2 { } private: + template struct has_format_hint { + private: + typedef char YesType[1]; + typedef char NoType[2]; + + template static YesType &test(decltype(&C::format_hint)); + template static NoType &test(...); + + public: + static constexpr bool value = sizeof(test(0)) == sizeof(YesType); + }; + + template struct has_purpose_hint { + private: + typedef char YesType[1]; + typedef char NoType[2]; + + template static YesType &test(decltype(&C::purpose_hint)); + template static NoType &test(...); + + public: + static constexpr bool value = sizeof(test(0)) == sizeof(YesType); + }; + template void add_argument_impl(ArgsMap const &argument_pairs) { std::unordered_map found_params{ @@ -132,19 +157,6 @@ namespace argument_parser::v2 { } if (argument_pairs.find(add_argument_flags::HelpText) != argument_pairs.end()) { help_text = get_or_throw(argument_pairs.at(add_argument_flags::HelpText), "help"); - } else { - help_text = ""; - if (short_arg != "-") { - help_text += short_arg; - } - - if (long_arg != "-") { - if (!help_text.empty()) { - help_text += ", "; - } - - help_text += long_arg; - } } if (argument_pairs.find(add_argument_flags::Required) != argument_pairs.end() && @@ -161,10 +173,34 @@ namespace argument_parser::v2 { if constexpr (IsTyped) { switch (suggested_add) { case candidate_type::typed_action: + if (help_text.empty()) { + if constexpr (has_format_hint>::value && + has_purpose_hint>::value) { + auto format_hint = parsing_traits::parser_trait::format_hint; + auto purpose_hint = parsing_traits::parser_trait::purpose_hint; + help_text = "Triggers action with " + std::string(purpose_hint) + " (" + + std::string(format_hint) + ")"; + } else { + help_text = "Triggers action with value."; + } + } + base::add_argument(short_arg, long_arg, help_text, *static_cast(&(*action)), required); break; case candidate_type::store_other: + if (help_text.empty()) { + if constexpr (has_format_hint>::value && + has_purpose_hint>::value) { + auto format_hint = parsing_traits::parser_trait::format_hint; + auto purpose_hint = parsing_traits::parser_trait::purpose_hint; + help_text = + "Accepts " + std::string(purpose_hint) + " in " + std::string(format_hint) + " format."; + } else { + help_text = "Accepts value."; + } + } + base::add_argument(short_arg, long_arg, help_text, required); break; default: @@ -173,10 +209,21 @@ namespace argument_parser::v2 { } else { switch (suggested_add) { case candidate_type::non_typed_action: + if (help_text.empty()) { + help_text = "Triggers action with no value."; + } + base::add_argument(short_arg, long_arg, help_text, *static_cast(&(*action)), required); break; case candidate_type::store_boolean: + if (help_text.empty()) { + auto boolPurpose = parsing_traits::parser_trait::purpose_hint; + auto boolFormat = parsing_traits::parser_trait::format_hint; + help_text = + "Accepts " + std::string(boolPurpose) + " in " + std::string(boolFormat) + " format."; + } + base::add_argument(short_arg, long_arg, help_text, required); break; default: diff --git a/src/headers/parser/parsing_traits/traits.hpp b/src/headers/parser/parsing_traits/traits.hpp index 63189c3..6efd12e 100644 --- a/src/headers/parser/parsing_traits/traits.hpp +++ b/src/headers/parser/parsing_traits/traits.hpp @@ -5,29 +5,43 @@ #include namespace argument_parser::parsing_traits { + using hint_type = const char *; + template struct parser_trait { using type = T_; static T_ parse(const std::string &input); + static constexpr hint_type format_hint = "value"; + static constexpr hint_type purpose_hint = "value"; }; template <> struct parser_trait { static std::string parse(const std::string &input); + static constexpr hint_type format_hint = "string"; + static constexpr hint_type purpose_hint = "string value"; }; template <> struct parser_trait { static bool parse(const std::string &input); + static constexpr hint_type format_hint = "true/false"; + static constexpr hint_type purpose_hint = "boolean value"; }; template <> struct parser_trait { static int parse(const std::string &input); + static constexpr hint_type format_hint = "123"; + static constexpr hint_type purpose_hint = "integer value"; }; template <> struct parser_trait { static float parse(const std::string &input); + static constexpr hint_type format_hint = "3.14"; + static constexpr hint_type purpose_hint = "flaoting point number"; }; template <> struct parser_trait { static double parse(const std::string &input); + static constexpr hint_type format_hint = "3.14"; + static constexpr hint_type purpose_hint = "double precision floating point number"; }; } // namespace argument_parser::parsing_traits diff --git a/src/main.cpp b/src/main.cpp index b798957..5422cd2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +#include "headers/parser/parsing_traits/traits.hpp" #include #define ALLOW_DASH_FOR_WINDOWS 0 @@ -22,12 +23,16 @@ template <> struct argument_parser::parsing_traits::parser_trait { int y = std::stoi(input.substr(comma_pos + 1)); return {x, y}; } + static constexpr argument_parser::parsing_traits::hint_type format_hint = "x,y"; + static constexpr argument_parser::parsing_traits::hint_type purpose_hint = "point coordinates"; }; template <> struct argument_parser::parsing_traits::parser_trait { static std::regex parse(const std::string &input) { return std::regex(input); } + static constexpr argument_parser::parsing_traits::hint_type format_hint = "regex"; + static constexpr argument_parser::parsing_traits::hint_type purpose_hint = "regular expression"; }; template <> struct argument_parser::parsing_traits::parser_trait> { @@ -40,6 +45,8 @@ template <> struct argument_parser::parsing_traits::parser_trait struct argument_parser::parsing_traits::parser_trait> { @@ -52,13 +59,16 @@ template <> struct argument_parser::parsing_traits::parser_trait conventions = { &argument_parser::conventions::gnu_argument_convention, &argument_parser::conventions::gnu_equal_argument_convention, - &argument_parser::conventions::windows_argument_convention, - &argument_parser::conventions::windows_equal_argument_convention}; + // &argument_parser::conventions::windows_argument_convention, + // &argument_parser::conventions::windows_equal_argument_convention +}; const auto echo = argument_parser::helpers::make_parametered_action( [](std::string const &text) { std::cout << text << std::endl; }); @@ -131,8 +141,7 @@ int v2Examples() { parser.add_argument( {{ShortArgument, "e"}, {LongArgument, "echo"}, {Action, echo}, {HelpText, "echoes given variable"}}); - parser.add_argument( - {{ShortArgument, "ep"}, {LongArgument, "echo-point"}, {Action, echo_point}, {HelpText, "echoes given point"}}); + parser.add_argument({{ShortArgument, "ep"}, {LongArgument, "echo-point"}, {Action, echo_point}}); parser.add_argument({ // stores string for f/file flag @@ -159,6 +168,8 @@ int v2Examples() { {Required, true} // makes this flag required }); + parser.add_argument({{ShortArgument, "v"}, {LongArgument, "verbose"}}); + parser.on_complete(::run_grep); parser.on_complete(::run_store_point); diff --git a/src/source/conventions/implementations/gnu_argument_convention.cpp b/src/source/conventions/implementations/gnu_argument_convention.cpp index a0b0e93..6bd704d 100644 --- a/src/source/conventions/implementations/gnu_argument_convention.cpp +++ b/src/source/conventions/implementations/gnu_argument_convention.cpp @@ -40,25 +40,26 @@ namespace argument_parser::conventions::implementations { return {}; // no fallback allowed } - std::string gnu_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, - bool requires_value) const { - std::string res = ""; + std::pair gnu_argument_convention::make_help_text(std::string const &short_arg, + std::string const &long_arg, + bool requires_value) const { + std::string s_part = ""; if (short_arg != "-" && short_arg != "") { - res += short_prec() + short_arg; + s_part += short_prec() + short_arg; if (requires_value) { - res += " "; + s_part += " "; } } + + std::string l_part = ""; if (long_arg != "-" && long_arg != "") { - if (!res.empty()) { - res += ", "; - } - res += long_prec() + long_arg; + l_part += long_prec() + long_arg; if (requires_value) { - res += " "; + l_part += " "; } } - return res; + + return {s_part, l_part}; } } // namespace argument_parser::conventions::implementations @@ -97,26 +98,26 @@ namespace argument_parser::conventions::implementations { return "--"; } - std::string gnu_equal_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, - bool requires_value) const { - std::string res = ""; + std::pair gnu_equal_argument_convention::make_help_text(std::string const &short_arg, + std::string const &long_arg, + bool requires_value) const { + std::string s_part = ""; if (short_arg != "-" && short_arg != "") { - res += short_prec() + short_arg; + s_part += short_prec() + short_arg; if (requires_value) { - res += "="; - } - } - if (long_arg != "-" && long_arg != "") { - if (!res.empty()) { - res += ", "; - } - res += long_prec() + long_arg; - if (requires_value) { - res += "="; + s_part += "="; } } - return res; + std::string l_part = ""; + if (long_arg != "-" && long_arg != "") { + l_part += long_prec() + long_arg; + if (requires_value) { + l_part += "="; + } + } + + return {s_part, l_part}; } std::vector gnu_equal_argument_convention::get_features() const { diff --git a/src/source/conventions/implementations/windows_argument_convention.cpp b/src/source/conventions/implementations/windows_argument_convention.cpp index 59a799b..bfd9fd1 100644 --- a/src/source/conventions/implementations/windows_argument_convention.cpp +++ b/src/source/conventions/implementations/windows_argument_convention.cpp @@ -50,27 +50,25 @@ namespace argument_parser::conventions::implementations { return "/"; } - std::string windows_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, + std::pair windows_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const { - std::string res = ""; + std::string s_part = ""; if (short_arg != "-" && short_arg != "") { - res += short_prec() + short_arg; + s_part += short_prec() + short_arg; if (requires_value) { - res += " "; + s_part += " "; } } + std::string l_part = ""; if (long_arg != "-" && long_arg != "") { - if (!res.empty()) { - res += ", "; - } - res += long_prec() + long_arg; + l_part += long_prec() + long_arg; if (requires_value) { - res += " "; + l_part += " "; } } - return res; + return {s_part, l_part}; } std::vector windows_argument_convention::get_features() const { @@ -130,30 +128,25 @@ namespace argument_parser::conventions::implementations { return "/"; } - std::string windows_kv_argument_convention::make_help_text(std::string const &short_arg, + std::pair windows_kv_argument_convention::make_help_text(std::string const &short_arg, std::string const &long_arg, bool requires_value) const { - std::string res = ""; + std::string s_part = ""; if (short_arg != "-" && short_arg != "") { - res += short_prec() + short_arg; + s_part += short_prec() + short_arg; if (requires_value) { - res += "="; - res += ", " + short_prec() + short_arg; - res += ":"; + s_part += "=, " + short_prec() + short_arg + ":"; } } + std::string l_part = ""; if (long_arg != "-" && long_arg != "") { - if (!res.empty()) { - res += ", "; - } - res += long_prec() + long_arg; + l_part += long_prec() + long_arg; if (requires_value) { - res += "=" - ", " + - long_prec() + long_arg + ":"; + l_part += "=, " + long_prec() + long_arg + ":"; } } - return res; + + return {s_part, l_part}; } std::vector windows_kv_argument_convention::get_features() const { diff --git a/src/source/parser/argument_parser.cpp b/src/source/parser/argument_parser.cpp index fee3e31..0c42b69 100644 --- a/src/source/parser/argument_parser.cpp +++ b/src/source/parser/argument_parser.cpp @@ -1,6 +1,7 @@ #include "argument_parser.hpp" #include +#include #include #include #include @@ -86,23 +87,56 @@ namespace argument_parser { std::stringstream ss; ss << "Usage: " << program_name << " [OPTIONS]...\n"; + size_t max_short_len = 0; + size_t max_long_len = 0; + + struct arg_help_info_t { + std::vector> convention_parts; + std::string desc; + }; + std::vector help_lines; + for (auto const &[id, arg] : argument_map) { auto short_arg = reverse_short_arguments.find(id) != reverse_short_arguments.end() ? reverse_short_arguments.at(id) : ""; auto long_arg = reverse_long_arguments.find(id) != reverse_long_arguments.end() ? reverse_long_arguments.at(id) : ""; - ss << "\t"; + std::vector> parts; std::unordered_set hasOnce; for (auto const &convention : convention_types) { - auto generatedHelpText = convention->make_help_text(short_arg, long_arg, arg.expects_parameter()); - if (hasOnce.find(generatedHelpText) == hasOnce.end()) { - ss << generatedHelpText << "\t"; - hasOnce.insert(generatedHelpText); + auto generatedParts = convention->make_help_text(short_arg, long_arg, arg.expects_parameter()); + std::string combined = generatedParts.first + "|" + generatedParts.second; + if (hasOnce.find(combined) == hasOnce.end()) { + parts.push_back(generatedParts); + hasOnce.insert(combined); + + if (generatedParts.first.length() > max_short_len) { + max_short_len = generatedParts.first.length(); + } + if (generatedParts.second.length() > max_long_len) { + max_long_len = generatedParts.second.length(); + } + } else { + parts.push_back({"", ""}); // trigger empty space in the help text } } - ss << arg.help_text << "\n"; + help_lines.push_back({parts, arg.help_text}); } + + for (auto const &line : help_lines) { + ss << "\t"; + for (size_t i = 0; i < line.convention_parts.size(); ++i) { + auto const &parts = line.convention_parts[i]; + if (i > 0) { + ss << " "; + } + ss << std::left << std::setw(static_cast(max_short_len)) << parts.first << " " + << std::setw(static_cast(max_long_len)) << parts.second; + } + ss << "\t" << line.desc << "\n"; + } + return ss.str(); } @@ -313,7 +347,18 @@ namespace argument_parser { for (auto const &[s, l, p] : required_args) { std::cerr << "\t" << get_one_name(s, l) << ": must be provided as one of ["; for (auto it = convention_types.begin(); it != convention_types.end(); ++it) { - std::cerr << (*it)->make_help_text(s, l, p); + auto generatedParts = (*it)->make_help_text(s, l, p); + std::string help_str = generatedParts.first; + if (!generatedParts.first.empty() && !generatedParts.second.empty()) { + help_str += " "; + } + help_str += generatedParts.second; + + size_t last_not_space = help_str.find_last_not_of(" \t"); + if (last_not_space != std::string::npos) { + help_str.erase(last_not_space + 1); + } + std::cerr << help_str; if (it + 1 != convention_types.end()) { std::cerr << ", "; }