From 81a85149b448f554676921a0c444bb976485f2a5 Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Mon, 4 May 2026 13:51:54 +0400 Subject: [PATCH 1/8] feat: new argument builder. base for ref. --- src/headers/parser/parser_v2.hpp | 3 +- src/headers/parser/parser_v3.hpp | 512 +++++++++++++++++++++++++++++++ 2 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 src/headers/parser/parser_v3.hpp diff --git a/src/headers/parser/parser_v2.hpp b/src/headers/parser/parser_v2.hpp index e64a577..abbe172 100644 --- a/src/headers/parser/parser_v2.hpp +++ b/src/headers/parser/parser_v2.hpp @@ -14,7 +14,7 @@ #include namespace argument_parser::v2 { - enum class add_argument_flags { ShortArgument, LongArgument, Positional, Position, HelpText, Action, Required }; + enum class add_argument_flags { ShortArgument, LongArgument, Positional, Position, HelpText, Action, Required, Reference }; namespace flags { constexpr static inline add_argument_flags ShortArgument = add_argument_flags::ShortArgument; @@ -24,6 +24,7 @@ namespace argument_parser::v2 { constexpr static inline add_argument_flags Required = add_argument_flags::Required; constexpr static inline add_argument_flags Positional = add_argument_flags::Positional; constexpr static inline add_argument_flags Position = add_argument_flags::Position; + constexpr static inline add_argument_flags Reference = add_argument_flags::Reference; } // namespace flags class base_parser : private argument_parser::base_parser { diff --git a/src/headers/parser/parser_v3.hpp b/src/headers/parser/parser_v3.hpp new file mode 100644 index 0000000..9cb5b8f --- /dev/null +++ b/src/headers/parser/parser_v3.hpp @@ -0,0 +1,512 @@ +#pragma once + +#include "argument_parser.hpp" +#include +#include +#include + +#ifndef ARGUMENT_PARSER_PARSER_V3_HPP +#define ARGUMENT_PARSER_PARSER_V3_HPP + +namespace argument_parser::builder { + +class non_type {}; +namespace builder_mask { + using v2_flag = argument_parser::v2::add_argument_flags; + using mask_type = std::uint64_t; + enum class value_mode { + unresolved, + store, + flag, + reference, + nonparametered_action, + parametered_action + }; + + enum class extra_capability : unsigned { + Store = static_cast(v2_flag::Reference) + 1, + Flag + }; + + constexpr auto bit(v2_flag flag) -> mask_type { + return mask_type{1} << static_cast(flag); + } + + constexpr auto bit(extra_capability capability) -> mask_type { + return mask_type{1} << static_cast(capability); + } + + constexpr mask_type short_argument = bit(v2_flag::ShortArgument); + constexpr mask_type long_argument = bit(v2_flag::LongArgument); + constexpr mask_type positional = bit(v2_flag::Positional); + constexpr mask_type position = bit(v2_flag::Position); + constexpr mask_type help_text = bit(v2_flag::HelpText); + constexpr mask_type action = bit(v2_flag::Action); + constexpr mask_type required = bit(v2_flag::Required); + constexpr mask_type reference = bit(v2_flag::Reference); + constexpr mask_type store = bit(extra_capability::Store); + constexpr mask_type flag = bit(extra_capability::Flag); + + constexpr mask_type value_mode_group = action | reference | store | flag; + constexpr mask_type initial = short_argument | long_argument | positional | help_text | action | required | + reference | store | flag; + + constexpr auto has(mask_type mask, mask_type capability) -> bool { + return (mask & capability) == capability; + } + + constexpr auto remove(mask_type mask, mask_type capability) -> mask_type { + return mask & ~capability; + } + + constexpr auto replace(mask_type mask, mask_type remove_bits, mask_type add_bits = 0) -> mask_type { + return (mask & ~remove_bits) | add_bits; + } + + constexpr auto has_selected_identifier(mask_type mask) -> bool { + return !has(mask, short_argument) || !has(mask, long_argument) || !has(mask, positional); + } + + constexpr auto is_buildable(mask_type mask) -> bool { + return has_selected_identifier(mask); + } +} // namespace builder_mask + +template +class argument { +public: + using mask_type = builder_mask::mask_type; + using v2_flag = argument_parser::v2::add_argument_flags; + using value_mode = builder_mask::value_mode; + + static auto start() -> argument { + return {}; + } + + template = 0> + auto short_argument(std::string short_name) const + -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_short_argument = std::move(short_name); + return next; + } + + template = 0> + auto long_argument(std::string long_name) const + -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_long_argument = std::move(long_name); + return next; + } + + template = 0> + auto positional(std::string positional_name) const + -> argument { + using next_argument = + argument; + + next_argument next{*this}; + next.m_positional_name = std::move(positional_name); + return next; + } + + template = 0> + auto position(int index) const -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_position = index; + return next; + } + + template = 0> + auto help_text(std::string help) const -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_help_text = std::move(help); + return next; + } + + template = 0> + auto required(bool value = true) const -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_required = value; + return next; + } + + template = 0> + auto store() const -> argument { + static_assert(!std::is_same_v, "store() is not supported. Use flag() for boolean-style arguments."); + + using next_argument = argument; + next_argument next{*this}; + next.m_value_mode = value_mode::store; + return next; + } + + template = 0> + auto flag() const -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_value_mode = value_mode::flag; + return next; + } + + template = 0, + typename T> + auto reference(T& value) const -> argument { + using next_argument = argument; + + next_argument next{*this}; + next.m_reference = static_cast(std::addressof(value)); + next.m_value_mode = value_mode::reference; + return next; + } + + template = 0, + typename Callable> + auto action(Callable&& handler) const + -> std::enable_if_t, + argument> { + using next_argument = + argument; + + next_argument next{*this}; + next.m_action = std::make_shared( + std::function(std::forward(handler))); + next.m_value_mode = value_mode::nonparametered_action; + return next; + } + + template = 0, typename Callable> + auto action(Callable&& handler) const + -> std::enable_if_t, + argument> { + static_assert(!std::is_same_v, "action(...) is not supported. Use action([] { ... }) instead."); + + using next_argument = argument; + + next_argument next{*this}; + next.m_action = std::make_shared>( + std::function(std::forward(handler))); + next.m_value_mode = value_mode::parametered_action; + return next; + } + + template = 0> + auto build(argument_parser::v2::base_parser& parser) const -> void { + assert_has_identifier(); + + switch (m_value_mode) { + case value_mode::flag: + build_flag(parser); + return; + case value_mode::nonparametered_action: + build_nonparametered_action(parser); + return; + case value_mode::store: + if constexpr (!std::is_same_v) { + build_store(parser); + return; + } + break; + case value_mode::reference: + if constexpr (!std::is_same_v) { + build_reference(parser); + return; + } + break; + case value_mode::parametered_action: + if constexpr (!std::is_same_v) { + build_parametered_action(parser); + return; + } + break; + case value_mode::unresolved: + if (is_positional()) { + build_default_positional(parser); + } else { + build_flag(parser); + } + return; + } + + throw std::logic_error("The builder reached build() without a supported terminal value mode."); + } + +private: + argument() = default; + + template + argument(argument const& other) + : m_short_argument(other.m_short_argument), + m_long_argument(other.m_long_argument), + m_positional_name(other.m_positional_name), + m_position(other.m_position), + m_help_text(other.m_help_text), + m_required(other.m_required), + m_action(other.m_action), + m_reference(other.m_reference), + m_value_mode(other.m_value_mode) {} + + template + using typed_map = std::unordered_map>; + + using non_typed_map = std::unordered_map; + + auto is_positional() const -> bool { + return !m_positional_name.empty(); + } + + auto lookup_key() const -> std::string { + if (is_positional()) { + return m_positional_name; + } + if (!m_long_argument.empty()) { + return m_long_argument; + } + if (!m_short_argument.empty()) { + return m_short_argument; + } + + throw std::logic_error("No argument identifier is available for lookup."); + } + + auto assert_has_identifier() const -> void { + if (!is_positional() && m_short_argument.empty() && m_long_argument.empty()) { + throw std::logic_error("build() requires short_argument(), long_argument(), or positional()."); + } + } + + template + auto add_common_pairs(Map& pairs) const -> void { + using namespace argument_parser::v2::flags; + + if (is_positional()) { + pairs[Positional] = m_positional_name; + if (m_position.has_value()) { + pairs[Position] = m_position.value(); + } + } else { + if (!m_short_argument.empty()) { + pairs[ShortArgument] = m_short_argument; + } + if (!m_long_argument.empty()) { + pairs[LongArgument] = m_long_argument; + } + } + + if (!m_help_text.empty()) { + pairs[HelpText] = m_help_text; + } + if (m_required) { + pairs[Required] = true; + } + } + + template + auto make_typed_pairs() const -> typed_map { + typed_map pairs; + add_common_pairs(pairs); + return pairs; + } + + auto make_non_typed_pairs() const -> non_typed_map { + non_typed_map pairs; + add_common_pairs(pairs); + return pairs; + } + + auto build_flag(argument_parser::v2::base_parser& parser) const -> void { + auto pairs = make_non_typed_pairs(); + parser.add_argument(pairs); + } + + auto build_default_positional(argument_parser::v2::base_parser& parser) const -> void { + auto pairs = make_non_typed_pairs(); + parser.add_argument(pairs); + } + + auto build_store(argument_parser::v2::base_parser& parser) const -> void { + auto pairs = make_typed_pairs(); + parser.template add_argument(pairs); + } + + auto build_reference(argument_parser::v2::base_parser& parser) const -> void { + auto pairs = make_typed_pairs(); + auto* target = static_cast(m_reference); + auto key = lookup_key(); + + if (target == nullptr) { + throw std::logic_error("reference() was selected without a target."); + } + + parser.template add_argument(pairs); + parser.on_complete([target, key](argument_parser::base_parser const& completed_parser) { + if (auto value = completed_parser.template get_optional(key)) { + *target = value.value(); + } + }); + } + + auto build_parametered_action(argument_parser::v2::base_parser& parser) const -> void { + auto const* typed_action = dynamic_cast const*>(m_action.get()); + if (typed_action == nullptr) { + throw std::logic_error("Stored action is not compatible with the requested parameter type."); + } + + auto pairs = make_typed_pairs(); + pairs[argument_parser::v2::flags::Action] = *typed_action; + parser.template add_argument(pairs); + } + + auto build_nonparametered_action(argument_parser::v2::base_parser& parser) const -> void { + auto const* nonparametered_action = dynamic_cast(m_action.get()); + if (nonparametered_action == nullptr) { + throw std::logic_error("Stored action is not a non-parametered action."); + } + + if (is_positional()) { + auto wrapped_action = argument_parser::helpers::make_parametered_action( + [action = *nonparametered_action](std::string const&) { action.invoke(); }); + auto pairs = make_typed_pairs(); + pairs[argument_parser::v2::flags::Action] = wrapped_action; + parser.template add_argument(pairs); + return; + } + + auto pairs = make_non_typed_pairs(); + pairs[argument_parser::v2::flags::Action] = *nonparametered_action; + parser.add_argument(pairs); + } + + std::string m_short_argument{}; + std::string m_long_argument{}; + std::string m_positional_name{}; + std::optional m_position{}; + std::string m_help_text{}; + bool m_required = false; + std::shared_ptr m_action{}; + void* m_reference = nullptr; + value_mode m_value_mode = value_mode::unresolved; + + template + friend class argument; +}; + +namespace assertions { + struct noop_handler { + void operator()() const {} + }; + + template + struct parameter_sink { + void operator()(T const&) const {} + }; + + template + struct can_use_help_text : std::false_type {}; + + template + struct can_use_help_text().help_text(std::declval()))>> : std::true_type {}; + + template + struct can_use_position : std::false_type {}; + + template + struct can_use_position().position(0))>> : std::true_type {}; + + template + struct can_use_nonparametered_action : std::false_type {}; + + template + struct can_use_nonparametered_action().action(noop_handler{}))>> : std::true_type {}; + + template + struct can_use_parametered_action : std::false_type {}; + + template + struct can_use_parametered_action().template action(parameter_sink{}))>> + : std::true_type {}; + + template + struct can_use_store : std::false_type {}; + + template + struct can_use_store().template store())>> : std::true_type {}; + + template + struct can_use_flag : std::false_type {}; + + template + struct can_use_flag().flag())>> : std::true_type {}; + + template + struct can_use_reference : std::false_type {}; + + template + struct can_use_reference().reference(std::declval()))>> : std::true_type {}; + + using after_help_text = decltype(argument<>::start().help_text("help")); + static_assert(!can_use_help_text::value, "help_text() should be single-use."); + + using after_positional = decltype(argument<>::start().positional("path")); + static_assert(can_use_position::value, "positional() should unlock position()."); + + using after_position = decltype(argument<>::start().positional("path").position(0)); + static_assert(!can_use_position::value, "position() should be single-use."); + + using after_positional_mode_selection = decltype(argument<>::start().positional("path").store<>()); + static_assert(!can_use_flag::value, "flag() should not be available for positional arguments."); + + using after_nonparametered_action = + decltype(argument<>::start().short_argument("v").help_text("verbose").action(noop_handler{})); + static_assert(!can_use_nonparametered_action::value, + "action() should not remain callable after selecting a non-parametered action."); + static_assert(!can_use_parametered_action::value, + "typed action() should also be disabled after selecting a non-parametered action."); + static_assert(!can_use_store::value, + "store() should be mutually exclusive with action()."); + static_assert(!can_use_flag::value, + "flag() should be mutually exclusive with action()."); + static_assert(!can_use_reference::value, + "reference() should be mutually exclusive with action()."); + + using after_parametered_action = + decltype(argument<>::start().positional("count").position(0).action(parameter_sink{})); + static_assert(!can_use_nonparametered_action::value, + "non-parametered action() should be disabled after selecting a typed action."); + static_assert(!can_use_parametered_action::value, + "typed action() should be single-use regardless of the parameter type."); + +} +} + +#endif From e1d72aaea77600b84bdc80074bcaf08b4d9811eb Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Mon, 4 May 2026 13:52:08 +0400 Subject: [PATCH 2/8] chore: initial examples folder. --- examples/test/CMakeLists.txt | 11 ++++++ examples/test/compile_commands.json | 8 +++++ examples/test/main.cpp | 56 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 examples/test/CMakeLists.txt create mode 100644 examples/test/compile_commands.json create mode 100644 examples/test/main.cpp diff --git a/examples/test/CMakeLists.txt b/examples/test/CMakeLists.txt new file mode 100644 index 0000000..8ec4221 --- /dev/null +++ b/examples/test/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) + +project(test) + +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(argument_parser REQUIRED) +add_executable(main main.cpp) +target_link_libraries(main argument_parser) diff --git a/examples/test/compile_commands.json b/examples/test/compile_commands.json new file mode 100644 index 0000000..f8de2ab --- /dev/null +++ b/examples/test/compile_commands.json @@ -0,0 +1,8 @@ +[ +{ + "directory": "/Users/killua/Projects/c++/argparser/argument-parser/examples/test/build", + "command": "/usr/bin/c++ -isystem /usr/local/include/argparse -isystem /usr/local/include/argparse/parser -isystem /usr/local/include/argparse/conventions -isystem /usr/local/include/argparse/conventions/implementations -isystem /usr/local/include/argparse/parser/platform_headers -isystem /usr/local/include/argparse/parser/parsing_traits -g -std=gnu++17 -arch arm64 -o CMakeFiles/main.dir/main.cpp.o -c /Users/killua/Projects/c++/argparser/argument-parser/examples/test/main.cpp", + "file": "/Users/killua/Projects/c++/argparser/argument-parser/examples/test/main.cpp", + "output": "/Users/killua/Projects/c++/argparser/argument-parser/examples/test/build/CMakeFiles/main.dir/main.cpp.o" +} +] diff --git a/examples/test/main.cpp b/examples/test/main.cpp new file mode 100644 index 0000000..bc1affc --- /dev/null +++ b/examples/test/main.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include +#include + +using argument = argument_parser::builder::argument<>; + +auto echo(std::string const& s) -> void { + std::cout << s << '\n'; +} + +auto main() -> int { + argument_parser::v2::parser parser(false); + + argument::start() + .positional("count") + .position(0) + .help_text("How many times to repeat the action.") + .required() + .action([](int const& count) { + std::cout << "count action configured for " << count << '\n'; + }) + .build(parser); + + int captured_value = 0; + argument::start() + .long_argument("threshold") + .help_text("Store the parsed value through a reference.") + .reference(captured_value) + .build(parser); + + argument::start() + .short_argument("q") + .help_text("Store a boolean flag.") + .build(parser); + + argument::start() + .long_argument("output") + .help_text("Store a string value.") + .store() + .build(parser); + + argument::start() + .short_argument("e") + .long_argument("echo") + .help_text("Echo the parsed value.") + .action(echo) + .build(parser); + + parser.handle_arguments({ + &argument_parser::conventions::gnu_argument_convention + }); + + return 0; +} From 7caeb20dfab6263e793791d370a8d867a74b6d1a Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Tue, 5 May 2026 10:36:12 +0400 Subject: [PATCH 3/8] feat: add compile time concatting capabilities for string building in parsing traits for hints. --- src/headers/argparse | 2 + .../{parser_v3.hpp => argument_builder.hpp} | 0 src/headers/parser/parsing_traits/traits.hpp | 38 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) rename src/headers/parser/{parser_v3.hpp => argument_builder.hpp} (100%) diff --git a/src/headers/argparse b/src/headers/argparse index d9376cd..ff8e28f 100644 --- a/src/headers/argparse +++ b/src/headers/argparse @@ -2,6 +2,8 @@ #ifndef ARGPARSE_HPP #define ARGPARSE_HPP #include +#include +#include #include "macros.h" #ifdef __linux__ diff --git a/src/headers/parser/parser_v3.hpp b/src/headers/parser/argument_builder.hpp similarity index 100% rename from src/headers/parser/parser_v3.hpp rename to src/headers/parser/argument_builder.hpp diff --git a/src/headers/parser/parsing_traits/traits.hpp b/src/headers/parser/parsing_traits/traits.hpp index c88664c..5e6bbfe 100644 --- a/src/headers/parser/parsing_traits/traits.hpp +++ b/src/headers/parser/parsing_traits/traits.hpp @@ -5,7 +5,7 @@ #include namespace argument_parser::parsing_traits { - using hint_type = const char *; + using hint_type = const char*; template struct parser_trait { using type = T_; @@ -50,6 +50,42 @@ namespace argument_parser::parsing_traits { static constexpr hint_type format_hint = "3.14"; static constexpr hint_type purpose_hint = "double precision floating point number"; }; + + + + constexpr hint_type comma = ","; + template + struct hint_provider { + static constexpr hint_type value = *PtrAddr; + }; + + template + struct joiner { + static constexpr auto get_combined() { + constexpr size_t total_len = (std::string_view{Providers::value}.length() + ... + 0); + + std::array arr{}; + size_t offset = 0; + + auto append = [&](hint_type s) { + std::string_view sv{s}; + for (char c : sv) arr[offset++] = c; + return 0; + }; + + (append(Providers::value), ...); + + arr[total_len] = '\0'; + return arr; + } + + static constexpr auto storage = get_combined(); + static constexpr hint_type value = storage.data(); + }; + + template + constexpr hint_type concat = joiner::value; + } // namespace argument_parser::parsing_traits #endif From 05c2f782b1b31eb73caedc96e33da980c9651935 Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Tue, 5 May 2026 10:36:24 +0400 Subject: [PATCH 4/8] chore: demontrate concat. --- examples/test/main.cpp | 59 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/examples/test/main.cpp b/examples/test/main.cpp index bc1affc..6492d5e 100644 --- a/examples/test/main.cpp +++ b/examples/test/main.cpp @@ -1,15 +1,49 @@ + #include #include -#include +#include #include -#include +#include +#include using argument = argument_parser::builder::argument<>; +using argument_parser::parsing_traits::hint_type; + auto echo(std::string const& s) -> void { std::cout << s << '\n'; } +using namespace argument_parser::parsing_traits; + +constexpr hint_type vector_purpose_hint = "vector of "; + +template +class parser_trait> { + public: + static std::vector parse(std::string const& s) { + std::vector result; + std::stringstream ss(s); + + for (std::string line; std::getline(ss, line, ',');) { + T item = parser_trait::parse(line); + result.push_back(item); + } + return result; + } + + ARGPARSE_TRAIT_FORMAT_HINT = concat< + hint_provider<&parser_trait::format_hint>, + hint_provider<&comma>, + hint_provider<&parser_trait::format_hint> + >; + + ARGPARSE_TRAIT_PURPOSE_HINT = concat< + hint_provider<&vector_purpose_hint>, + hint_provider<&parser_trait::purpose_hint> + >; +}; + auto main() -> int { argument_parser::v2::parser parser(false); @@ -17,7 +51,6 @@ auto main() -> int { .positional("count") .position(0) .help_text("How many times to repeat the action.") - .required() .action([](int const& count) { std::cout << "count action configured for " << count << '\n'; }) @@ -35,11 +68,11 @@ auto main() -> int { .help_text("Store a boolean flag.") .build(parser); - argument::start() - .long_argument("output") - .help_text("Store a string value.") - .store() - .build(parser); + // argument::start() + // .long_argument("regex") + // .help_text("Store a regex value.") + // .store>() + // .build(parser); argument::start() .short_argument("e") @@ -48,6 +81,16 @@ auto main() -> int { .action(echo) .build(parser); + argument::start() + .long_argument("vecstr") + .short_argument("vs") + .action>([](std::vector const& vecstr) { + for (auto const& str : vecstr) { + std::cout << str << '\n'; + } + }) + .build(parser); + parser.handle_arguments({ &argument_parser::conventions::gnu_argument_convention }); From faf1715ee33fab9f040ff1a453056d46f894e71c Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Tue, 5 May 2026 10:38:19 +0400 Subject: [PATCH 5/8] chore: remove compile commands --- examples/test/compile_commands.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 examples/test/compile_commands.json diff --git a/examples/test/compile_commands.json b/examples/test/compile_commands.json deleted file mode 100644 index f8de2ab..0000000 --- a/examples/test/compile_commands.json +++ /dev/null @@ -1,8 +0,0 @@ -[ -{ - "directory": "/Users/killua/Projects/c++/argparser/argument-parser/examples/test/build", - "command": "/usr/bin/c++ -isystem /usr/local/include/argparse -isystem /usr/local/include/argparse/parser -isystem /usr/local/include/argparse/conventions -isystem /usr/local/include/argparse/conventions/implementations -isystem /usr/local/include/argparse/parser/platform_headers -isystem /usr/local/include/argparse/parser/parsing_traits -g -std=gnu++17 -arch arm64 -o CMakeFiles/main.dir/main.cpp.o -c /Users/killua/Projects/c++/argparser/argument-parser/examples/test/main.cpp", - "file": "/Users/killua/Projects/c++/argparser/argument-parser/examples/test/main.cpp", - "output": "/Users/killua/Projects/c++/argparser/argument-parser/examples/test/build/CMakeFiles/main.dir/main.cpp.o" -} -] From 708f63a00d426f3ffef54f663ae31e6ab9803ac4 Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Tue, 5 May 2026 11:20:53 +0400 Subject: [PATCH 6/8] feat: implement reference capture mode. --- examples/test/main.cpp | 11 +++++++++++ src/headers/parser/argument_builder.hpp | 17 +++++++++++++---- src/headers/parser/parser_v2.hpp | 22 +++++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/examples/test/main.cpp b/examples/test/main.cpp index 6492d5e..09561d1 100644 --- a/examples/test/main.cpp +++ b/examples/test/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,8 @@ class parser_trait> { >; }; +using namespace argument_parser::v2::flags; + auto main() -> int { argument_parser::v2::parser parser(false); @@ -63,6 +66,12 @@ auto main() -> int { .reference(captured_value) .build(parser); + // parser.add_argument({ + // {ShortArgument, "c"}, + // {HelpText, "capture count"}, + // {Reference, &captured_value}, + // }); + argument::start() .short_argument("q") .help_text("Store a boolean flag.") @@ -95,5 +104,7 @@ auto main() -> int { &argument_parser::conventions::gnu_argument_convention }); + std::cout << "captured value: " << captured_value << '\n'; + return 0; } diff --git a/src/headers/parser/argument_builder.hpp b/src/headers/parser/argument_builder.hpp index 9cb5b8f..2de870c 100644 --- a/src/headers/parser/argument_builder.hpp +++ b/src/headers/parser/argument_builder.hpp @@ -184,7 +184,7 @@ public: using next_argument = argument; next_argument next{*this}; - next.m_reference = static_cast(std::addressof(value)); + next.m_reference = std::addressof(value); next.m_value_mode = value_mode::reference; return next; } @@ -273,7 +273,7 @@ private: m_help_text(other.m_help_text), m_required(other.m_required), m_action(other.m_action), - m_reference(other.m_reference), + m_reference(copy_reference(other.m_reference)), m_value_mode(other.m_value_mode) {} template @@ -361,7 +361,7 @@ private: auto build_reference(argument_parser::v2::base_parser& parser) const -> void { auto pairs = make_typed_pairs(); - auto* target = static_cast(m_reference); + auto* target = m_reference; auto key = lookup_key(); if (target == nullptr) { @@ -414,9 +414,18 @@ private: std::string m_help_text{}; bool m_required = false; std::shared_ptr m_action{}; - void* m_reference = nullptr; + store_type* m_reference = nullptr; value_mode m_value_mode = value_mode::unresolved; + template + static auto copy_reference(other_store_type* reference) -> store_type* { + if constexpr (std::is_same_v) { + return reference; + } else { + return nullptr; + } + } + template friend class argument; }; diff --git a/src/headers/parser/parser_v2.hpp b/src/headers/parser/parser_v2.hpp index abbe172..bfea633 100644 --- a/src/headers/parser/parser_v2.hpp +++ b/src/headers/parser/parser_v2.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -29,7 +30,7 @@ namespace argument_parser::v2 { class base_parser : private argument_parser::base_parser { public: - template using typed_flag_value = std::variant, bool, int>; + template using typed_flag_value = std::variant, bool, int, T*>; using non_typed_flag_value = std::variant; template using typed_argument_pair = std::pair>; @@ -145,6 +146,22 @@ namespace argument_parser::v2 { required = true; } + if (argument_pairs.find(add_argument_flags::Reference) != argument_pairs.end()) { + if (!IsTyped) { + throw std::logic_error("Reference argument must be typed"); + } + + found_params[extended_add_argument_flags::Action] = true; + if constexpr (!std::is_same_v) { + auto ref = get_or_throw(argument_pairs.at(add_argument_flags::Reference), "reference"); + action = helpers::make_parametered_action([ref](T const& t) { + *ref = t; + }).clone(); + } else { + throw std::logic_error("Reference argument must not be void"); + } + } + auto suggested_add = suggest_candidate(found_params); if (suggested_add == candidate_type::unknown) { throw std::runtime_error("Could not match any add argument overload to given parameters. Are you " @@ -166,8 +183,7 @@ namespace argument_parser::v2 { } } - base::add_argument(short_arg, long_arg, help_text, *static_cast(&(*action)), - required); + base::add_argument(short_arg, long_arg, help_text, *static_cast(&(*action)), required); break; case candidate_type::store_other: if (help_text.empty()) { From 1479892e7ba6b4c522e4805715b65823d9c0a4cc Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Tue, 5 May 2026 11:27:48 +0400 Subject: [PATCH 7/8] chore: update todo. --- TODO.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index e4b96d5..46ec87e 100644 --- a/TODO.md +++ b/TODO.md @@ -46,12 +46,38 @@ instead of an action doing it. # TODO 7: Defaults/Implicits If given, an arguments default store value could be changed. If nothing was given use that value instead. -# TODO 8: Validators +# TODO 8: Validators | DONE If given, validate the argument before passing to the storage or action. If fail, let user decide fail loud or fail skip. # TODO 9: Subcommand/Subactions Implement subcommand support. Users should be able to define subactions to the higher level action. For example, ```cpp -parser.add_argument( - {{ShortArgument, "l"}, {LongArgument, "list"}, {Action, list_files}, {HelpText, "Lists files in the directory"}}); +parser.add_argument( + {{ShortArgument, "g"}, {LongArgument, "get"}, {Action, get_}, {HelpText, "Gets "}} +); +parser.add_argument( + {{BaseArgument, "g"}, {ShortArgument, "f"}, {LongArgument, "files"}, {Action, get_files}, {HelpText, "Gets files"}} +); +... +``` + +# TODO 10: Reference capture | DONE +Reference capturing. +```cpp +parser.add_argument({ + {ShortArgument, "c"}, + {HelpText, "capture value"}, + {Reference, &captured_value}, +}); +``` + +# TODO 11: Builder | DONE +Implement type safe logic enforcing argument builder; +```cpp +argument::start() + .short_argument("e") + .long_argument("echo") + .help_text("Echo the parsed value.") + .action(echo) + .build(parser); ``` From 1c63622fd8054f819e5838b1f6448566a4210695 Mon Sep 17 00:00:00 2001 From: "killua.z" Date: Tue, 5 May 2026 11:44:18 +0400 Subject: [PATCH 8/8] chore: update readme.md --- README.md | 221 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 157 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 2e21e8d..3916e6f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,21 @@ # argument-parser -A lightweight, modern, expressively typed, and highly customizable C++17 argument parser library. +A lightweight, modern, and highly customizable C++17 argument parser with native platform argument collection, trait-driven typed parsing, pluggable option conventions, and a fluent `v2` builder API. + +> `v1` is deprecated and mainly kept as implementation history. For new projects, use `argument_parser::v2` together with `argument_parser::builder`. ## 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! +- Native platform parser alias: `argument_parser::v2::parser` resolves to the current platform parser and reads arguments directly from OS APIs. +- Fluent builder API with compile-time builder constraints that prevent invalid combinations after a terminal/mutually exclusive mode has been selected. +- Type-safe parsing and extraction. Just extend `parser_trait` for your types and if just want to store use `get_optional()`! +- Positional arguments with optional explicit ordering and support for `--` as a positional separator. +- Trait-driven `format_hint` and `purpose_hint` metadata used in generated help text and parse errors. +- Automatic help flag on `argument_parser::v2::parser` (`-h`, `--help`) with configurable exit behavior. +- Auto-formatted help output.. +- Completion hooks via `parser.on_complete(...)`. +- Pluggable conventions for GNU next-token, GNU equal-style, Windows next-token, and Windows inline `=` / `:` parsing, or bring your own! +- Testing helper + pseudo command handler `argument_parser::v2::fake_parser`. ## Requirements @@ -20,104 +24,193 @@ V1 is deprecated and is mainly kept as a base implementation for the V2. You sho ## Quick Start -### 1. Create your Parser and Define Arguments - ```cpp +#include #include #include -#include -#include // Provides the native parser for your compiling platform + +using argument = argument_parser::builder::argument<>; 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; + argument_parser::v2::parser parser(false); // --help prints without exiting immediately - // 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"} - }); + int threshold = 0; - // A flag that just stores the value to extract later - parser.add_argument({ - {ShortArgument, "g"}, - {LongArgument, "grep"}, - {HelpText, "Grep pattern"} - }); + argument::start() + .short_argument("e") + .long_argument("echo") + .action([](std::string const& text) { + std::cout << text << '\n'; + }) + .build(parser); - // A required flag - parser.add_argument({ - {LongArgument, "file"}, - {Required, true}, - {HelpText, "File to grep"} - }); + argument::start() + .long_argument("file") + .store() + .required() + .help_text("Input file to process.") + .build(parser); - // 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; + argument::start() + .long_argument("threshold") + .reference(threshold) + .build(parser); + + argument::start() + .short_argument("v") + .long_argument("verbose") + .flag() + .help_text("Enable verbose output.") + .build(parser); + + argument::start() + .positional("output") + .position(0) + .help_text("Output file.") + .build(parser); + + parser.on_complete([](auto const& state) { + if (auto file = state.template get_optional("file")) { + std::cout << "completed for: " << *file << '\n'; } }); - // Register Conventions const std::initializer_list conventions = { &argument_parser::conventions::gnu_argument_convention, - &argument_parser::conventions::windows_argument_convention + &argument_parser::conventions::gnu_equal_argument_convention, + &argument_parser::conventions::windows_argument_convention, + &argument_parser::conventions::windows_equal_argument_convention, }; - // Execute logic! parser.handle_arguments(conventions); - return 0; + if (auto file = parser.get_optional("file")) { + std::cout << "file: " << *file << '\n'; + } + + std::cout << "threshold: " << threshold << '\n'; } ``` -### 2. Custom Type Parsing +## Trait-Driven Parsing and Hints -You can natively parse your custom structs, objects, or arrays by specializing `argument_parser::parsing_traits::parser_trait`. +Specialize `argument_parser::parsing_traits::parser_trait` to add support for your own types and to describe their expected format. ```cpp +#include +#include +#include +#include + struct Point { - int x, y; + int x; + int 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}; +template <> +struct argument_parser::parsing_traits::parser_trait { + static Point parse(std::string const& input) { + auto comma = input.find(','); + if (comma == std::string::npos) { + throw std::runtime_error("Expected x,y"); + } + + return { + std::stoi(input.substr(0, comma)), + std::stoi(input.substr(comma + 1)) + }; } -}; -// Now you can directly use your type: -// parser.add_argument({ {LongArgument, "point"} }); -// auto point = parser.get_optional("point"); + ARGPARSE_TRAIT_FORMAT_HINT = "x,y"; + ARGPARSE_TRAIT_PURPOSE_HINT = "point coordinates"; +}; +``` + +Then use the type directly from the builder: + +```cpp +argument::start() + .long_argument("point") + .store() + .build(parser); +``` + +If you omit `help_text()`, `v2` uses the trait hints to generate help such as `Accepts point coordinates in x,y format.` The same hints are also included in type conversion errors. + +## Help Behavior + +`argument_parser::v2::parser` automatically registers `-h` and `--help`. + +```cpp +argument_parser::v2::parser parser; // help prints and exits +argument_parser::v2::parser parser(false); // help prints without immediate exit +``` + +You can also display help manually: + +```cpp +parser.display_help(conventions); +``` + +## Supported Conventions + +- GNU next-token: `-o value`, `--output value` +- GNU equal-style: `-o=value`, `--output=value` +- Windows next-token: `/output value` +- Windows inline value: `/output=value`, `/output:value` + +Mix any of them in the same parser by passing the conventions you want to `handle_arguments()`. + +## Builder Modes + +`argument_parser::builder::argument<>` is a staged builder. `build(parser)` is the terminal call. + +Before `build(...)`, you compose an argument from three kinds of steps: + +- Identifier selection: `short_argument(...)`, `long_argument(...)`, or `positional(...)` +- Optional metadata: `position(...)` for positional arguments, `help_text(...)`, and `required(...)` +- One mutually exclusive value behavior: + - `store()` to parse and retain a value for later `get_optional()` + - `flag()` to store a boolean presence flag + - `reference(value)` to write the parsed result directly into an existing variable + - `action([] { ... })` for no-value callbacks + - `action([](T const&) { ... })` for typed value callbacks + +Once you select one value behavior, the other value behavior methods are disabled at compile time, so combinations like `store().action(...)` or `flag().reference(value)` are rejected by the type system. Also you cannot use the same method repeatedly as it is also disabled at compile time by the type system. + +If you do not select a value behavior explicitly, `build(parser)` uses the default for the argument kind: named arguments become boolean flags, while positional arguments store a `std::string`. + +## Testing + +For unit tests or synthetic argument lists, use `argument_parser::v2::fake_parser` instead of the native platform parser: + +```cpp +#include + +argument_parser::v2::fake_parser parser("tool", {"--count", "3", "input.txt"}); ``` ## CMake Integration -The library can be installed globally via CMake or incorporated into your project. +Use the project directly: ```cmake add_subdirectory(argument-parser) target_link_libraries(your_target PRIVATE argument_parser) ``` +Or install and consume it as a package: + +```cmake +find_package(argument_parser CONFIG REQUIRED) +target_link_libraries(your_target PRIVATE argument_parser::argument_parser) +``` + ## Building & Installing ```bash mkdir build && cd build cmake .. cmake --build . +cmake --install . ```