feat: improve help text for better readability, less duplication on the conventions, better syntax information.

This commit is contained in:
2026-03-16 20:53:00 +04:00
parent 675e81e67b
commit d5b99ef407
10 changed files with 332 additions and 24 deletions

View File

@@ -1,11 +1,13 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
#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<argument_type, std::string>;
@@ -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<convention_features> get_features() const = 0;
protected:
base_convention() = default;

View File

@@ -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<convention_features> 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<convention_features> get_features() const override;
static gnu_equal_argument_convention instance;
private:

View File

@@ -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<convention_features> 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<convention_features> get_features() const override;
static windows_kv_argument_convention instance;
private:

View File

@@ -256,7 +256,7 @@ namespace argument_parser {
std::optional<argument> &found_help);
void invoke_arguments(std::unordered_map<std::string, std::string> const &values_for_arguments,
std::vector<std::pair<std::string, argument>> const &found_arguments,
std::vector<std::pair<std::string, argument>> &found_arguments,
std::optional<argument> const &found_help);
void enforce_creation_thread();

View File

@@ -120,10 +120,10 @@ namespace argument_parser::v2 {
found_params[extended_add_argument_flags::LongArgument] = true;
long_arg = get_or_throw<std::string>(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<std::string>(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() &&

View File

@@ -4,7 +4,6 @@
#include <argparse>
#include <fstream>
#include <iostream>
#include <parser_v2.hpp>
#include <regex>
#include <sstream>
#include <vector>
@@ -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;

View File

@@ -35,6 +35,31 @@ namespace argument_parser::conventions::implementations {
std::string gnu_argument_convention::long_prec() const {
return "--";
}
std::vector<convention_features> 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 += " <value>";
}
}
if (long_arg != "-" && long_arg != "") {
if (!res.empty()) {
res += ", ";
}
res += long_prec() + long_arg;
if (requires_value) {
res += " <value>";
}
}
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 += "=<value>";
}
}
if (long_arg != "-" && long_arg != "") {
if (!res.empty()) {
res += ", ";
}
res += long_prec() + long_arg;
if (requires_value) {
res += "=<value>";
}
}
return res;
}
std::vector<convention_features> gnu_equal_argument_convention::get_features() const {
return {}; // no fallback allowed
}
} // namespace argument_parser::conventions::implementations

View File

@@ -1,4 +1,5 @@
#include "windows_argument_convention.hpp"
#include "base_convention.hpp"
#include <stdexcept>
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 += " <value>";
}
}
if (long_arg != "-" && long_arg != "") {
if (!res.empty()) {
res += ", ";
}
res += long_prec() + long_arg;
if (requires_value) {
res += " <value>";
}
}
return res;
}
std::vector<convention_features> 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 += "=<value>";
res += ", " + short_prec() + short_arg;
res += ":<value>";
}
}
if (long_arg != "-" && long_arg != "") {
if (!res.empty()) {
res += ", ";
}
res += long_prec() + long_arg;
if (requires_value) {
res += "=<value>"
", " +
long_prec() + long_arg + ":<value>";
}
}
return res;
}
std::vector<convention_features> 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

View File

@@ -4,8 +4,10 @@
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<std::string> 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<std::string, std::string> const &values_for_arguments,
std::vector<std::pair<std::string, argument>> const &found_arguments,
std::vector<std::pair<std::string, argument>> &found_arguments,
std::optional<argument> 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<conventions::convention const *const> convention_types) {
std::vector<std::pair<std::string, std::string>> required_args;
std::vector<std::tuple<std::string, std::string, bool>> required_args;
for (auto const &[key, arg] : argument_map) {
if (arg.is_required() && !arg.is_invoked()) {
required_args.emplace_back<std::pair<std::string, std::string>>(
{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<std::tuple<std::string, std::string, bool>>(
{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);