From 8e502bcb8bc7b8e5b3be7939a15da75454f1dbc2 Mon Sep 17 00:00:00 2001 From: killua Date: Mon, 16 Mar 2026 21:50:05 +0400 Subject: [PATCH] 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 << ", "; }