From dcfe2fd290b11dbb0fb2c884c5658fd26cdf7d0e Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 27 Sep 2026 13:58:51 +0200 Subject: [PATCH 1/2] version handling refactor --- docs/tutorial.md | 24 +++--------- include/argon/argument_parser.hpp | 45 ++++++++++------------- include/argon/types.hpp | 18 --------- tests/source/test_argument_parser_cfg.cpp | 41 +++++++++------------ 4 files changed, 42 insertions(+), 86 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index e49a1f9d..f246d628 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -167,20 +167,6 @@ parser.version("alpha") > * [Argument Groups](#argument-groups) - organize related optional arguments into sections and optionally enforce usage rules. > * [Subparsers](#subparsers) - create hierarchical CLI subcommand structures. -> [!TIP] -> You can specify the program version using a string (like in the example above) or using the `argon::version` structure: -> ```cpp -> parser.version({0u, 0u, 0u}) -> parser.version({ .major = 1u, .minor = 1u, .patch = 1u }); -> argon::version ver{2u, 2u, 2u}; -> parser.version(ver); -> ``` -> -> **NOTE:** The `argon::version` struct -> * contains the three members - `major`, `minor`, `patch` - all of which are of type `std::uint32_t`, -> * defines a `std::string str() const` method which returns a `v{major}.{minor}.{path}` version string, -> * defines the `std::ostream& operator<<` for stream insertion. - ### Dynamic Program Name If you prefer not to hardcode the program name, you can instruct the parser to dynamically deduce it from the command-line input (specifically `argv[0]`) by initializing it with the `argon::dynamic_name` tag: @@ -427,7 +413,7 @@ parser.add_optional_argument("version", "v").suppress_arg_checks(); parser.parse_args(argc, argv); -if (parser.count("version")) { +if (parser.is_used("version")) { std::cout << PROJECT_VERSION << std::endl; std::exit(EXIT_SUCCESS); } @@ -832,7 +818,7 @@ parser.default_arguments(); parser.print_version(); std::exit(EXIT_SUCCESS); }) - .help("Display program version info"); + .help("Display program's version info"); ``` - `o_input` and `o_multi_input`: @@ -1103,7 +1089,7 @@ int main(int argc, char* argv[]) { argon::argument_parser parser("some-program"); // define the parser's attributes and default arguments - parser.version({0u, 0u, 0u}) + parser.version("v0.1.0") .description("The program does something with command-line arguments") .default_arguments(argon::default_argument::o_help); @@ -1647,7 +1633,7 @@ For example: ```cpp // top-level parser argon::argument_parser git("ap-git"); -git.version({.major = 2u, .minor = 43u, .patch = 0u}) +git.version("v2.43.0") .description("A version control system built with CPP-ARGON") .default_arguments(argon::default_argument::o_help, argon::default_argument::o_version); @@ -1686,7 +1672,7 @@ All defined subparsers will be included in the parent parser's help message: ```txt > ap-git --help -Program: ap-git (v2.43.0) +Program: ap-git v2.43.0 A version control system built with CPP-ARGON diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index c53ee8f8..22860690 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -75,7 +75,7 @@ enum class default_argument : std::uint8_t { * arg_parser.print_version(); * std::exit(EXIT_SUCCESS); * }) - * .help("Dsiplay program version info"); + * .help("Dsiplay program's version info"); * @endcode */ o_version, @@ -154,7 +154,7 @@ void add_default_argument(const default_argument, argument_parser&) noexcept; * int main(int argc, char* argv[]) { * // Create the argument parser instance * argon::argument_parser parser("fcopy"); - * parser.version({ .major = 1, .minor = 0, .patch = 0 }) + * parser.version("v0.1.0") * .description("A simple file copy utility.") * .default_arguments( * argon::default_argument::o_help, @@ -215,28 +215,20 @@ class argument_parser { * @brief Set the program version. * @param version The version of the program. * @return Reference to the argument parser. - */ - argument_parser& version(const version& version) noexcept { - this->_version.emplace(version.str()); - return *this; - } - - /** - * @brief Set the program version. - * @param version The version of the program. - * @return Reference to the argument parser. + * @throws argon::invalid_configuration if the given version string contains whitespaces. */ argument_parser& version(std::string_view version) { - if (util::contains_whitespaces(version)) - throw invalid_configuration("The program version cannot contain whitespace characters!" - ); + for (const char c : version) + if (std::iscntrl(static_cast(c))) + throw invalid_configuration("The program version cannot contain control characters " + "(line breaks, tabs, etc.)!"); - this->_version.emplace(version); + this->_version = version; return *this; } - /// @return The program version if set, std::nullopt otherwise. - [[nodiscard]] const std::optional& version() const noexcept { + /// @return The program version if set, empty string otherwise. + [[nodiscard]] const std::string& version() const noexcept { return this->_version; } @@ -983,8 +975,8 @@ class argument_parser { */ void print_help(const bool verbose, std::ostream& os = std::cout) const noexcept { os << "Program: " << this->_program_name; - if (this->_version) - os << " (" << this->_version.value() << ')'; + if (not this->_version.empty()) + os << ' ' << this->_version; os << '\n'; if (not this->_description.empty()) @@ -998,13 +990,16 @@ class argument_parser { /** * @brief Prints the argument parser's version info to an output stream. * - * If no version was spcified for the parser, `unspecified` will be printed. + * If no version was specified for the parser, `(version unspecified)` will be printed. * * @param os The output stream. */ void print_version(std::ostream& os = std::cout) const noexcept { - os << this->_program_name << " : version " << this->_version.value_or("unspecified") - << std::endl; + os << this->_program_name; + if (this->_version.empty()) + os << " (version unspecified)" << std::endl; + else + os << ' ' << this->_version << std::endl; } /** @@ -1634,7 +1629,7 @@ class argument_parser { std::string _program_name = ""; // The name of the program in the format "... ". std::string _description = ""; - std::optional _version = std::nullopt; + std::string _version = ""; unknown_policy _unknown_policy = unknown_policy::fail; @@ -1727,7 +1722,7 @@ inline void add_default_argument( arg_parser.print_version(); std::exit(EXIT_SUCCESS); }) - .help("Dsiplay program version info"); + .help("Dsiplay program's version info"); break; case default_argument::o_input: diff --git a/include/argon/types.hpp b/include/argon/types.hpp index 60d44fe2..fc335f04 100644 --- a/include/argon/types.hpp +++ b/include/argon/types.hpp @@ -27,22 +27,4 @@ struct dynamic_name_t { /// @brief Tag value that enables dynamic deduction of the program name. inline constexpr dynamic_name_t dynamic_name{}; -/// @brief A helper structure used to represent a program's version. -struct version { - std::uint32_t major = 0u; ///< The major version number. - std::uint32_t minor = 0u; ///< The minor version number. - std::uint32_t patch = 0u; ///< The patch number. - - /// @brief Converts the structure into a string in the `v{major}.{minor}.{path}` format - [[nodiscard]] std::string str() const noexcept { - return std::format("v{}.{}.{}", this->major, this->minor, this->patch); - } - - /// @brief The stream insertion operator. - friend std::ostream& operator<<(std::ostream& os, const version& v) { - os << v.str(); - return os; - } -}; - } // namespace argon diff --git a/tests/source/test_argument_parser_cfg.cpp b/tests/source/test_argument_parser_cfg.cpp index 469fc7d0..c6d5690d 100644 --- a/tests/source/test_argument_parser_cfg.cpp +++ b/tests/source/test_argument_parser_cfg.cpp @@ -10,8 +10,7 @@ TEST_SUITE_BEGIN("test_argument_parser_cfg"); struct test_argument_parser_cfg : public argument_parser_test_fixture { const std::string test_description = "test program description"; - const argon::version test_version{1u, 2u, 3u}; - const std::string test_str_version = "alpha"; + const std::string test_version = "v1.2.3"; }; TEST_CASE("argument_parser() should throw if the name is empty") { @@ -49,31 +48,29 @@ TEST_CASE_FIXTURE( // --- version --- -TEST_CASE_FIXTURE(test_argument_parser_cfg, "version() getter should return nullopt by default") { - CHECK_FALSE(sut.version()); +TEST_CASE_FIXTURE(test_argument_parser_cfg, "version() setter should update the version member") { + CHECK(sut.version().empty()); // default value + + sut.version(test_version); + CHECK_EQ(sut.version(), test_version); } TEST_CASE_FIXTURE( test_argument_parser_cfg, - "version() setter should throw if the version string contains whitespaces" + "version() setter should throw if the version string contains control characters" ) { - CHECK_THROWS_WITH_AS( - sut.version("invalid version"), - "The program version cannot contain whitespace characters!", - invalid_configuration - ); + const std::string expected_msg = + "The program version cannot contain control characters (line breaks, tabs, etc.)!"; + + CHECK_THROWS_WITH_AS(sut.version("1.0\nAlpha"), expected_msg.c_str(), invalid_configuration); + CHECK_THROWS_WITH_AS(sut.version("1.0\tAlpha"), expected_msg.c_str(), invalid_configuration); } -TEST_CASE_FIXTURE(test_argument_parser_cfg, "version() setter should update the version member") { - sut.version(test_version); - auto stored_version = sut.version(); - REQUIRE(stored_version); - CHECK_EQ(stored_version.value(), test_version.str()); +TEST_CASE_FIXTURE(test_argument_parser_cfg, "version() setter should allow standard spaces") { + const std::string version_with_spaces = "v0.1.0 alpha"; - sut.version(test_str_version); - stored_version = sut.version(); - REQUIRE(stored_version); - CHECK_EQ(stored_version.value(), test_str_version); + REQUIRE_NOTHROW(sut.version(version_with_spaces)); + CHECK_EQ(sut.version(), version_with_spaces); } // --- description --- @@ -88,11 +85,7 @@ TEST_CASE_FIXTURE( test_argument_parser_cfg, "description() setter should update the description member" ) { sut.description(test_description); - - const auto stored_description = sut.description(); - - REQUIRE_FALSE(stored_description.empty()); - CHECK_EQ(stored_description, test_description); + CHECK_EQ(sut.description(), test_description); } // --- is_verbose --- From 0c01cd71ffa4483d8fb85b70d205eb62071903de Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 27 Sep 2026 14:03:45 +0200 Subject: [PATCH 2/2] resolved comments + demo update --- cpp-argon-demo | 2 +- docs/tutorial.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp-argon-demo b/cpp-argon-demo index c0cdbaf3..37dab817 160000 --- a/cpp-argon-demo +++ b/cpp-argon-demo @@ -1 +1 @@ -Subproject commit c0cdbaf36fd344a7d44523dd2a171ebd3365f17e +Subproject commit 37dab8178630db1a393bc0ce79078b5b3a04934b diff --git a/docs/tutorial.md b/docs/tutorial.md index f246d628..8b99c09d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1633,7 +1633,7 @@ For example: ```cpp // top-level parser argon::argument_parser git("ap-git"); -git.version("v2.43.0") +git.version("v0.1.0") .description("A version control system built with CPP-ARGON") .default_arguments(argon::default_argument::o_help, argon::default_argument::o_version); @@ -1672,7 +1672,7 @@ All defined subparsers will be included in the parent parser's help message: ```txt > ap-git --help -Program: ap-git v2.43.0 +Program: ap-git v0.1.0 A version control system built with CPP-ARGON