From d5b99ef407c6f7b209a6078e03aa1b625a77dac5 Mon Sep 17 00:00:00 2001 From: killua Date: Mon, 16 Mar 2026 20:53:00 +0400 Subject: [PATCH] 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);