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.

This commit is contained in:
2026-03-16 21:50:05 +04:00
parent d5b99ef407
commit 8e502bcb8b
9 changed files with 190 additions and 79 deletions

View File

@@ -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<std::string, std::string> make_help_text(std::string const &short_arg, std::string const &long_arg,
bool requires_value) const = 0;
virtual std::vector<convention_features> get_features() const = 0;

View File

@@ -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<std::string, std::string> make_help_text(std::string const &short_arg, std::string const &long_arg,
bool requires_value) const override;
std::vector<convention_features> 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<std::string, std::string> make_help_text(std::string const &short_arg, std::string const &long_arg,
bool requires_value) const override;
std::vector<convention_features> get_features() const override;

View File

@@ -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<std::string, std::string> make_help_text(std::string const &short_arg, std::string const &long_arg,
bool requires_value) const override;
std::vector<convention_features> 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<std::string, std::string> make_help_text(std::string const &short_arg, std::string const &long_arg,
bool requires_value) const override;
std::vector<convention_features> get_features() const override;

View File

@@ -1,4 +1,5 @@
#pragma once
#include "traits.hpp"
#include <argument_parser.hpp>
#include <array>
#include <cstdlib>
@@ -103,6 +104,30 @@ namespace argument_parser::v2 {
}
private:
template <typename T> struct has_format_hint {
private:
typedef char YesType[1];
typedef char NoType[2];
template <typename C> static YesType &test(decltype(&C::format_hint));
template <typename> static NoType &test(...);
public:
static constexpr bool value = sizeof(test<T>(0)) == sizeof(YesType);
};
template <typename T> struct has_purpose_hint {
private:
typedef char YesType[1];
typedef char NoType[2];
template <typename C> static YesType &test(decltype(&C::purpose_hint));
template <typename> static NoType &test(...);
public:
static constexpr bool value = sizeof(test<T>(0)) == sizeof(YesType);
};
template <bool IsTyped, typename ActionType, typename T, typename ArgsMap>
void add_argument_impl(ArgsMap const &argument_pairs) {
std::unordered_map<extended_add_argument_flags, bool> 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<std::string>(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<parsing_traits::parser_trait<T>>::value &&
has_purpose_hint<parsing_traits::parser_trait<T>>::value) {
auto format_hint = parsing_traits::parser_trait<T>::format_hint;
auto purpose_hint = parsing_traits::parser_trait<T>::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<ActionType *>(&(*action)),
required);
break;
case candidate_type::store_other:
if (help_text.empty()) {
if constexpr (has_format_hint<parsing_traits::parser_trait<T>>::value &&
has_purpose_hint<parsing_traits::parser_trait<T>>::value) {
auto format_hint = parsing_traits::parser_trait<T>::format_hint;
auto purpose_hint = parsing_traits::parser_trait<T>::purpose_hint;
help_text =
"Accepts " + std::string(purpose_hint) + " in " + std::string(format_hint) + " format.";
} else {
help_text = "Accepts value.";
}
}
base::add_argument<T>(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<ActionType *>(&(*action)),
required);
break;
case candidate_type::store_boolean:
if (help_text.empty()) {
auto boolPurpose = parsing_traits::parser_trait<bool>::purpose_hint;
auto boolFormat = parsing_traits::parser_trait<bool>::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:

View File

@@ -5,29 +5,43 @@
#include <string>
namespace argument_parser::parsing_traits {
using hint_type = const char *;
template <typename T_> 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<std::string> {
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<bool> {
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<int> {
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<float> {
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<double> {
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

View File

@@ -1,3 +1,4 @@
#include "headers/parser/parsing_traits/traits.hpp"
#include <string>
#define ALLOW_DASH_FOR_WINDOWS 0
@@ -22,12 +23,16 @@ template <> struct argument_parser::parsing_traits::parser_trait<Point> {
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<std::regex> {
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<std::vector<int>> {
@@ -40,6 +45,8 @@ template <> struct argument_parser::parsing_traits::parser_trait<std::vector<int
}
return result;
}
static constexpr argument_parser::parsing_traits::hint_type format_hint = "int,int,int";
static constexpr argument_parser::parsing_traits::hint_type purpose_hint = "list of integers";
};
template <> struct argument_parser::parsing_traits::parser_trait<std::vector<std::string>> {
@@ -52,13 +59,16 @@ template <> struct argument_parser::parsing_traits::parser_trait<std::vector<std
}
return result;
}
static constexpr argument_parser::parsing_traits::hint_type format_hint = "string,string,string";
static constexpr argument_parser::parsing_traits::hint_type purpose_hint = "list of strings";
};
const std::initializer_list<argument_parser::conventions::convention const *const> 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>(
[](std::string const &text) { std::cout << text << std::endl; });
@@ -131,8 +141,7 @@ int v2Examples() {
parser.add_argument<std::string>(
{{ShortArgument, "e"}, {LongArgument, "echo"}, {Action, echo}, {HelpText, "echoes given variable"}});
parser.add_argument<Point>(
{{ShortArgument, "ep"}, {LongArgument, "echo-point"}, {Action, echo_point}, {HelpText, "echoes given point"}});
parser.add_argument<Point>({{ShortArgument, "ep"}, {LongArgument, "echo-point"}, {Action, echo_point}});
parser.add_argument<std::string>({
// 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);

View File

@@ -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,
std::pair<std::string, 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::string s_part = "";
if (short_arg != "-" && short_arg != "") {
res += short_prec() + short_arg;
s_part += short_prec() + short_arg;
if (requires_value) {
res += " <value>";
s_part += " <value>";
}
}
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 += " <value>";
l_part += " <value>";
}
}
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,
std::pair<std::string, 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::string s_part = "";
if (short_arg != "-" && short_arg != "") {
res += short_prec() + short_arg;
s_part += short_prec() + short_arg;
if (requires_value) {
res += "=<value>";
}
}
if (long_arg != "-" && long_arg != "") {
if (!res.empty()) {
res += ", ";
}
res += long_prec() + long_arg;
if (requires_value) {
res += "=<value>";
s_part += "=<value>";
}
}
return res;
std::string l_part = "";
if (long_arg != "-" && long_arg != "") {
l_part += long_prec() + long_arg;
if (requires_value) {
l_part += "=<value>";
}
}
return {s_part, l_part};
}
std::vector<convention_features> gnu_equal_argument_convention::get_features() const {

View File

@@ -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<std::string, 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 = "";
std::string s_part = "";
if (short_arg != "-" && short_arg != "") {
res += short_prec() + short_arg;
s_part += short_prec() + short_arg;
if (requires_value) {
res += " <value>";
s_part += " <value>";
}
}
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 += " <value>";
l_part += " <value>";
}
}
return res;
return {s_part, l_part};
}
std::vector<convention_features> 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<std::string, 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 = "";
std::string s_part = "";
if (short_arg != "-" && short_arg != "") {
res += short_prec() + short_arg;
s_part += short_prec() + short_arg;
if (requires_value) {
res += "=<value>";
res += ", " + short_prec() + short_arg;
res += ":<value>";
s_part += "=<value>, " + short_prec() + short_arg + ":<value>";
}
}
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 += "=<value>"
", " +
long_prec() + long_arg + ":<value>";
l_part += "=<value>, " + long_prec() + long_arg + ":<value>";
}
}
return res;
return {s_part, l_part};
}
std::vector<convention_features> windows_kv_argument_convention::get_features() const {

View File

@@ -1,6 +1,7 @@
#include "argument_parser.hpp"
#include <functional>
#include <iomanip>
#include <iostream>
#include <optional>
#include <sstream>
@@ -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<std::pair<std::string, std::string>> convention_parts;
std::string desc;
};
std::vector<arg_help_info_t> 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<std::pair<std::string, std::string>> parts;
std::unordered_set<std::string> 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<int>(max_short_len)) << parts.first << " "
<< std::setw(static_cast<int>(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 << ", ";
}