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

123
README.md Normal file
View File

@@ -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<int>`, `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 <iostream>
#include <string>
#include <regex>
#include <argparse> // 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<std::string>({
{ShortArgument, "e"},
{LongArgument, "echo"},
{Action, argument_parser::helpers::make_parametered_action<std::string>(
[](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<std::regex>({
{ShortArgument, "g"},
{LongArgument, "grep"},
{HelpText, "Grep pattern"}
});
// A required flag
parser.add_argument<std::string>({
{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<std::string>("file");
auto pattern = p.get_optional<std::regex>("grep");
if (filename && pattern) {
std::cout << "Grepping " << filename.value() << " with pattern." << std::endl;
}
});
// Register Conventions
const std::initializer_list<argument_parser::conventions::convention const *const> 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<T>`.
```cpp
struct Point {
int x, y;
};
template <> struct argument_parser::parsing_traits::parser_trait<Point> {
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<Point>({ {LongArgument, "point"} });
// auto point = parser.get_optional<Point>("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 .
```

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() {
try {
return v2Examples();
try {
} 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;
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);