From 0ff49c99e7f522f79643b2a2557f0c20f235ac8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:47:49 +0200 Subject: [PATCH] YT-CPPAP-8: Fix typos and rename some elements - Fixed some typos and incorrect examples in the readme file - Added markdown alerts in the readme file - Renamed and refactored the `ap::action::check_file_exists_action()` function - Improved the istream operator in example program: verbosity --- README.md | 110 +++++---- change_log.md | 3 +- example/source/verbosity.cpp | 22 +- include/ap/argument_parser.hpp | 419 ++++++++++++++++----------------- 4 files changed, 281 insertions(+), 273 deletions(-) diff --git a/README.md b/README.md index 7bf39c7..8b94108 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,19 @@ Command-line argument parser for C++20 ## Overview -The `CPP-AP` library has been developed for the *Team Programming* course at the *Wrocław University of Science and Technology*. - -Faculty: *W04N - Faculty of Information and Communication Technology* - -Field of study: *Algorithmic Computer Science* - -
- The goal of the project was to create a light, intuitive and simple to use command-line argument parser library for the `C++20` and newer standards. The `CPP-AP` library does not require installing any additional tools or heavy libraries, like with `boost::program_options`. Much like with the `Doctest` framework - the only thing you need to do is copy the `argument_parser.hpp` file into the include directory of your project and you're set to go. +> [!NOTE] +> [v1.0](https://github.com/SpectraL519/cpp-ap/commit/9a9e5360766b732f322ae2efe3cf5ec5f9268eef) of the library has been developed for the *Team Programming* course at the *Wrocław University of Science and Technology*. +> +> Faculty: *W04N - Faculty of Information and Communication Technology* +> +> Field of study: *Algorithmic Computer Science* +> +> The project has received the 1st place at the 2024 CreatiWITy competition organized by the faculty. The article in Polish can be found on the [faculty website](https://wit.pwr.edu.pl/aktualnosci/oto-laureaci-konkursu-creatiwity-273.html). Please note that this is not a technical article :) +

@@ -74,25 +75,28 @@ parser.program_name("Name of the program") The parser supports both positional and optional arguments. Both argument types are identified by their names represented as strings. Arguments can be defined with only a primary name or with a primary and a secondary (short) name. -**NOTE:** The basic rules of parsing positional and optional arguments are described in the [Parsing arguments](#parsing-arguments) section. +> [!NOTE] +> The basic rules of parsing positional and optional arguments are described in the [Parsing arguments](#parsing-arguments) section. To add an argument to the parameter's configurations use the following syntax: ```c++ -parser.add__argument("argument_name"); +parser.add__argument("argument"); ``` or ```c++ -parser.add__argument("argument_name", "a"); +parser.add__argument("argument", "a"); ``` -**NOTE:** The library supports any argument value types which meet the following requirements: -* The `std::ostream& operator<<` must be overloaded for a value type -* The type must have a copy constructor and an assignment operator +> [!NOTE] +> The library supports any argument value types which meet the following requirements: +* The `std::ostream& operator<<` is overloaded for the value type +* The value type has a copy constructor and an assignment operator -**NOTE:** If the `value_type` is not provided, `std::string` will be used. +> [!IMPORTANT] +> If the `value_type` is not provided, `std::string` will be used. You can also add boolean flags: @@ -135,12 +139,13 @@ Parameters which can be specified for both positional and optional arguments inc * `choices` - a list of valid argument values. The `choices` parameter takes a `const std::vector&` as an argument. - **NOTE:** To use the `choices` the `value_type` must overload the equaility comparison operator: `==`; - ```c++ parser.add_optional_argument("method", "m").choices({'a', 'b', 'c'}); ``` +> [!IMPORTANT] +> To use the `choices` the `value_type` must overload the equaility comparison operator: `==`; + * `action` - a function performed after reading an argument's value. Actions are represented as functions, which take the argument's value as an argument. There are two types of actions: @@ -160,10 +165,10 @@ Parameters which can be specified for both positional and optional arguments inc .action([](const double& value) { return 1. / value; }); ``` - Actions can also be used to perform some value checking logic, e.g. the predefined `check_file_exists_action` which checks if a file with a given name exists: + Actions can also be used to perform some value checking logic, e.g. the predefined `check_file_exists` which checks if a file with a given name exists: ```c++ parser.add_optional_argument("input", "i") - .action(ap::action::check_file_exists_action); + .action(ap::action::check_file_exists()); ``` **Optional argument specific parameters** @@ -196,7 +201,8 @@ Parameters which can be specified for both positional and optional arguments inc parser.add_optional_argument("input", "i").nargs(ap::nargs::any()); ``` - **NOTE:** The default nargs value is `1`. +> [!NOTE] +> The default nargs value is `1`. * `default_value` - the default value for an argument which will be used if no values for this argument are parsed ```c++ @@ -231,7 +237,7 @@ The supported default arguments are: ```c++ // equivalent to: parser.add_positional_argument("input") - .action(ap::action::check_file_exists_action) + .action(ap::action::check_file_exists()) .help("Input file path"); ``` @@ -247,14 +253,15 @@ The supported default arguments are: parser.add_flag("help", "h").bypass_required().help("Display help message"); ``` - **Note:** As of now the *on flag action* functionality is not implemented in the library - this will be added in a future release. - To properly use the help argument in the current release add the following right beneath the `parser.parse_args(argc, argv)` try-catch block: - ```c++ - if (parser.has_value("help")) { - std::cout << parser << std::endl; - std::exit(EXIT_SUCCESS); - } - ``` +> [!NOTE] +> As of now the *on flag action* functionality is not implemented in the library - this will be added in a future release. +> To properly use the help argument in the current release add the following right beneath the `parser.parse_args(argc, argv)` try-catch block: +> ```c++ +> if (parser.value("help")) { +> std::cout << parser << std::endl; +> std::exit(EXIT_SUCCESS); +> } +> ``` * `optional::input` and `optional::multi_input`: ```c++ @@ -262,14 +269,14 @@ The supported default arguments are: parser.add_optional_argument("input", "i") .required() .nargs(1) - .action(ap::action::check_file_exists_action) + .action(ap::action::check_file_exists()) .help("Input file path"); // multi_input - equivalent to: parser.add_optional_argument("input", "i") .required() .nargs(ap::nargs::at_least(1)) - .action(ap::action::check_file_exists_action) + .action(ap::action::check_file_exists()) .help("Input files paths"); ``` @@ -288,7 +295,8 @@ The supported default arguments are: .help("Output files paths"); ``` -**NOTE:** The `argument_parser::default__arguments` functions will be modified to use a variadic argument list instead of a `std::vector` in a future release. +> [!NOTE] +> The `argument_parser::default__arguments` functions will be modified to use a variadic argument list instead of a `std::vector` in a future release. ### Parsing arguments @@ -298,14 +306,14 @@ To parse the command-line arguments use the `argument_parser::parse_args` method // power.cpp #include -#include #include +#include int main(int argc, char* argv[]) { // create the parser class instance ap::argument_parser parser; parser.program_name("power calculator") - .program_description("calculates the value of an expression: base & exponent"); + .program_description("calculates the value of an expression: base ^ exponent"); // add arguments parser.add_positional_argument("base").help("the exponentation base value"); @@ -325,7 +333,7 @@ int main(int argc, char* argv[]) { } // check for the help argument presence - if (parser.has_value("help")) { + if (parser.value("help")) { std::cout << parser << std::endl; std::exit(EXIT_SUCCESS); } @@ -361,18 +369,20 @@ int main(int argc, char* argv[]) { # no exponent values given ``` - **NOTE:** For each positional argument there must be **exactly one value**. ```shell - ./test_program + ./power # out: # [ERROR] : No values parsed for a required argument [base] # power calculator - # calculates the value of an expression: base & exponent + # calculates the value of an expression: base ^ exponent # [base] : the exponentation base value # [exponent,e] : the exponent value # [help,h] : Display help message ``` +> [!IMPORTANT] +> For each positional argument there must be **exactly one value**. + * Optional arguments are parsed only with a flag: ```shell ./power 2 --exponent 1 2 3 @@ -400,7 +410,8 @@ int main(int argc, char* argv[]) { # [help,h] : Display help message ``` -**NOTE:** The parser behaviour depends on the argument definitions. The argument parameters are described int the [Argument parameters](#argument-parameters) section. +> [!IMPORTANT] +> The parser behaviour depends on the argument definitions. The argument parameters are described int the [Argument parameters](#argument-parameters) section.

@@ -416,7 +427,8 @@ cd /example The examples' source files are in the `/example/source` directory. -> **Note:** Each source file is a sepparate example. +> [!NOTE] +> Each source file is a sepparate example. Building the examples: @@ -461,11 +473,13 @@ cmake .. make ``` -> **NOTE:** Building on Windows - use the `-G "Unix Makefiles"` option when running CMake to build a GNU Make project instead of a default Visual Studio project. +> [!TIP] +> Building on Windows - use the `-G "Unix Makefiles"` option when running CMake to build a GNU Make project instead of a default Visual Studio project. Run the tests: -> **NOTE:** The test executable is generated in the `/test/build` directory. +> [!NOTE] +> The test executable is generated in the `/test/build` directory. * All tests: @@ -479,18 +493,21 @@ Run the tests: ./test -ts="" ``` - > **Note**: Test suites in the project have the same name as the files they're in. + > [!NOTE] + > Test suites in the project have the same names as the files they're in.
### Formatting -> **NOTE:** To ensure new line encoding compatibility the project uses unix new line encoding. +> [!IMPORTANT] +> To ensure new line encoding compatibility the project uses unix new line encoding. > > This can be set using the `git config --global core.autocrlf true` command. > More details can be found [here](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings) -> **NOTE:** The project uses `clang-format-17`. +> [!NOTE] +> The project uses `clang-format-17`. > > To install this tool on ubuntu run `sudo bash ./scripts/env/install_clang17_toolchain.sh`. > @@ -534,7 +551,8 @@ The documentation for this project can be generated using Doxygen: As of now the project supports the **GNU G++** and **Clang++** compilers with `C++20` support on Linux and Windows. -> **NOTE:** To build the project using clang you will need to install the `clang-17` toolchain using the script or website mentioned in the [Formatting](#formatting) section. +> [!NOTE] +> To build the project using clang you will need to install the `clang-17` toolchain using the script or website mentioned in the [Formatting](#formatting) section.

diff --git a/change_log.md b/change_log.md index 74727e9..c56f156 100644 --- a/change_log.md +++ b/change_log.md @@ -30,9 +30,10 @@ * Aligned the `.clang-format` configuration file * Added the `install_clang17_toolchain.sh` env script * Added the `format` workflow -* Switched from the `` to the `` library for all current container operations +* Switched to the `std::ranges` and `std::views` algorithms for all current container operations * Modified the `argument_name` structure - renamed members: `name` to `primary`, `short_name` to `secondary` * Added `argument_name::match(string_view)` and `argument_name::match(argument_name)` functions * Added aliases for default argument enum classes: * `ap::default_argument::positional` = `ap::default_posarg` * `ap::default_argument::optional` = `ap::default_optarg` +* Renamed the predefined: `ap::action::check_file_exists_action` -> `ap::action::check_file_exists` diff --git a/example/source/verbosity.cpp b/example/source/verbosity.cpp index c100a0e..366853e 100644 --- a/example/source/verbosity.cpp +++ b/example/source/verbosity.cpp @@ -6,33 +6,23 @@ namespace { enum class verbosity_level : uint16_t { low, mid, high }; +constexpr verbosity_level max_verbosity_level = verbosity_level::high; -std::istream& operator>>(std::istream& input, verbosity_level& v) { +std::istream& operator>>(std::istream& input, verbosity_level& verbosity) { uint16_t value; input >> value; - // Map the integer input to the corresponding enum value - switch (value) { - case 0u: - v = verbosity_level::low; - break; - - case 1u: - v = verbosity_level::mid; - break; - - case 2u: - v = verbosity_level::high; - break; - - default: + if (value > static_cast(max_verbosity_level)) { std::cerr << "[ERROR] : Invalid verbosity_level value - " << value << std::endl; std::exit(EXIT_FAILURE); } + verbosity = static_cast(value); + return input; } + void print_msg(const verbosity_level verbosity) { switch (verbosity) { case verbosity_level::low: diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 2ed25a3..e82cbbe 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -106,213 +106,6 @@ inline constexpr bool is_valid_type_v = std::disjunction_v_default; - } - - /** - * @brief Checks if a given value count is within the range. - * @param n The value count to check. - * @return Ordering relationship between the count and the range. - */ - [[nodiscard]] std::weak_ordering contains(const range::count_type n) const noexcept { - if (not (this->_nlow.has_value() or this->_nhigh.has_value())) - return std::weak_ordering::equivalent; - - if (this->_nlow.has_value() and this->_nhigh.has_value()) { - if (n < this->_nlow.value()) - return std::weak_ordering::less; - - if (n > this->_nhigh.value()) - return std::weak_ordering::greater; - - return std::weak_ordering::equivalent; - } - - if (this->_nlow.has_value()) - return (n < this->_nlow.value()) ? std::weak_ordering::less : std::weak_ordering::equivalent; - - return (n > this->_nhigh.value()) ? std::weak_ordering::greater : std::weak_ordering::equivalent; - } - - friend range at_least(const count_type) noexcept; - friend range more_than(const count_type) noexcept; - friend range less_than(const count_type) noexcept; - friend range up_to(const count_type) noexcept; - friend range any() noexcept; - -private: - /** - * @brief Private constructor: creates a possibly unbound range - * @param nlow The optional lower bound of the range. - * @param nhigh The optional upper bound of the range. - */ - range(const std::optional nlow, const std::optional nhigh) - : _nlow(nlow), _nhigh(nhigh) {} - - std::optional _nlow; - std::optional _nhigh; - bool _default = true; - - static constexpr count_type _ndefault = 1; -}; - -/** - * @brief `range` class builder function. Creates a range [n, inf]. - * @param n The lower bound. - * @return Built `range` class instance. - */ -[[nodiscard]] inline range at_least(const range::count_type n) noexcept { - return range(n, std::nullopt); -} - -/** - * @brief `range` class builder function. Creates a range [n + 1, inf]. - * @param n The lower bound. - * @return Built `range` class instance. - */ -[[nodiscard]] inline range more_than(const range::count_type n) noexcept { - return range(n + 1, std::nullopt); -} - -/** - * @brief `range` class builder function. Creates a range [0, n - 1]. - * @param n The upper bound - * @return Built `range` class instance. - */ -[[nodiscard]] inline range less_than(const range::count_type n) noexcept { - return range(std::nullopt, n - 1); -} - -/** - * @brief `range` class builder function. Creates a range [0, n]. - * @param n The upper bound - * @return Built `range` class instance. - */ -[[nodiscard]] inline range up_to(const range::count_type n) noexcept { - return range(std::nullopt, n); -} - -/** - * @brief `range` class builder function. Creates a range [0, inf]. - * @return Built `range` class instance. - */ -[[nodiscard]] inline range any() noexcept { - return range(std::nullopt, std::nullopt); -} - -} // namespace nargs - -/// @brief Defines valued argument action traits. -struct valued_action { - template - using type = std::function; -}; - -/// @brief Defines void argument action traits. -struct void_action { - template - using type = std::function; -}; - -// TODO: on_read_action - -/// @brief Argument action handling utility. -namespace action { - -/// @brief Internal argument action handling utility -namespace detail { - -/** - * @brief The concept is satisfied when `AS` is either a valued or void argument action - * @tparam AS The action specifier type. - */ -template -concept valid_action_specifier = ap::utility::is_valid_type_v; - -/// @brief Template argument action callable type alias. -template -using callable_type = typename AS::template type; - -/// @brief Template argument action callabla variant type alias. -template -using action_variant_type = - std::variant, callable_type>; - -/** - * @brief Checks if an argument action variant holds a void action. - * @tparam T The argument value type. - * @param action The action variant. - * @return True if the held action is a void action. - */ -template -[[nodiscard]] inline bool is_void_action(const action_variant_type& action) noexcept { - return std::holds_alternative>(action); -} - -} // namespace detail - -/// @brief Returns a default argument action. -template -detail::callable_type default_action() noexcept { - return [](T&) {}; -} - -/// @brief Returns a predefined action for file name handling arguments. Checks whether a file with the given name exists. -inline detail::callable_type check_file_exists_action() noexcept { - return [](std::string& file_path) { - if (not std::filesystem::exists(file_path)) { - std::cerr << "[ERROR] : File " + file_path + " does not exists!"; - std::exit(EXIT_FAILURE); - } - }; -} - -// TODO: on_flag_action - -} // namespace action - /// @brief Internal argument handling utility. namespace argument::detail { @@ -623,6 +416,212 @@ class invalid_nvalues_error : public argument_parser_error { } // namespace error +/// @brief Argument's number of values management utility. +namespace nargs { + +/// @brief Argument's number of values managing class. +class range { +public: + using count_type = std::size_t; + + /// @brief Default constructor: creates range [1, 1]. + range() : _nlow(_ndefault), _nhigh(_ndefault) {} + + /** + * @brief Exact count constructor: creates range [n, n]. + * @param n Expected value count. + */ + explicit range(const count_type n) : _nlow(n), _nhigh(n), _default(n == _ndefault) {} + + /** + * @brief Concrete range constructor: creates range [nlow, nhigh]. + * @param nlow The lower bound. + * @param nhigh The upper bound. + */ + range(const count_type nlow, const count_type nhigh) + : _nlow(nlow), _nhigh(nhigh), _default(nlow == _ndefault and nhigh == _ndefault) {} + + /// @brief Copy constructor + range(const range&) = default; + + /// @brief Move constructor + range(range&&) = default; + + /// @brief Copy assignmner constructor + range& operator=(const range&) = default; + + /// @brief Move assignmner constructor + range& operator=(range&&) = default; + + /// @brief Class destructor. + ~range() = default; + + /// @return True if the range is [1, 1]. + [[nodiscard]] bool is_default() const noexcept { + return this->_default; + } + + /** + * @brief Checks if a given value count is within the range. + * @param n The value count to check. + * @return Ordering relationship between the count and the range. + */ + [[nodiscard]] std::weak_ordering contains(const range::count_type n) const noexcept { + if (not (this->_nlow.has_value() or this->_nhigh.has_value())) + return std::weak_ordering::equivalent; + + if (this->_nlow.has_value() and this->_nhigh.has_value()) { + if (n < this->_nlow.value()) + return std::weak_ordering::less; + + if (n > this->_nhigh.value()) + return std::weak_ordering::greater; + + return std::weak_ordering::equivalent; + } + + if (this->_nlow.has_value()) + return (n < this->_nlow.value()) ? std::weak_ordering::less : std::weak_ordering::equivalent; + + return (n > this->_nhigh.value()) ? std::weak_ordering::greater : std::weak_ordering::equivalent; + } + + friend range at_least(const count_type) noexcept; + friend range more_than(const count_type) noexcept; + friend range less_than(const count_type) noexcept; + friend range up_to(const count_type) noexcept; + friend range any() noexcept; + +private: + /** + * @brief Private constructor: creates a possibly unbound range + * @param nlow The optional lower bound of the range. + * @param nhigh The optional upper bound of the range. + */ + range(const std::optional nlow, const std::optional nhigh) + : _nlow(nlow), _nhigh(nhigh) {} + + std::optional _nlow; + std::optional _nhigh; + bool _default = true; + + static constexpr count_type _ndefault = 1; +}; + +/** + * @brief `range` class builder function. Creates a range [n, inf]. + * @param n The lower bound. + * @return Built `range` class instance. + */ +[[nodiscard]] inline range at_least(const range::count_type n) noexcept { + return range(n, std::nullopt); +} + +/** + * @brief `range` class builder function. Creates a range [n + 1, inf]. + * @param n The lower bound. + * @return Built `range` class instance. + */ +[[nodiscard]] inline range more_than(const range::count_type n) noexcept { + return range(n + 1, std::nullopt); +} + +/** + * @brief `range` class builder function. Creates a range [0, n - 1]. + * @param n The upper bound + * @return Built `range` class instance. + */ +[[nodiscard]] inline range less_than(const range::count_type n) noexcept { + return range(std::nullopt, n - 1); +} + +/** + * @brief `range` class builder function. Creates a range [0, n]. + * @param n The upper bound + * @return Built `range` class instance. + */ +[[nodiscard]] inline range up_to(const range::count_type n) noexcept { + return range(std::nullopt, n); +} + +/** + * @brief `range` class builder function. Creates a range [0, inf]. + * @return Built `range` class instance. + */ +[[nodiscard]] inline range any() noexcept { + return range(std::nullopt, std::nullopt); +} + +} // namespace nargs + +/// @brief Defines valued argument action traits. +struct valued_action { + template + using type = std::function; +}; + +/// @brief Defines void argument action traits. +struct void_action { + template + using type = std::function; +}; + +// TODO: +// * on_read_action +// * on_flag_action + +/// @brief Argument action handling utility. +namespace action { + +/// @brief Internal argument action handling utility +namespace detail { + +/** + * @brief The concept is satisfied when `AS` is either a valued or void argument action + * @tparam AS The action specifier type. + */ +template +concept valid_action_specifier = ap::utility::is_valid_type_v; + +/// @brief Template argument action callable type alias. +template +using callable_type = typename AS::template type; + +/// @brief Template argument action callabla variant type alias. +template +using action_variant_type = + std::variant, callable_type>; + +/** + * @brief Checks if an argument action variant holds a void action. + * @tparam T The argument value type. + * @param action The action variant. + * @return True if the held action is a void action. + */ +template +[[nodiscard]] inline bool is_void_action(const action_variant_type& action) noexcept { + return std::holds_alternative>(action); +} + +} // namespace detail + +/// @brief Returns a default argument action. +template +detail::callable_type default_action() noexcept { + return [](T&) {}; +} + +/// @brief Returns a predefined action for file name handling arguments. Checks whether a file with the given name exists. +inline detail::callable_type check_file_exists() noexcept { + return [](std::string& file_path) { + if (not std::filesystem::exists(file_path)) + throw argument_parser_error("[ERROR] : File " + file_path + " does not exists!"); + }; +} + +} // namespace action + + /// @brief Namespace containing classes and utilities for handling command-line arguments. namespace argument { @@ -1431,7 +1430,7 @@ class argument_parser { switch (arg_discriminator) { case default_posarg::input: this->add_positional_argument("input") - .action(ap::action::check_file_exists_action()) + .action(ap::action::check_file_exists()) .help("Input file path"); break; @@ -1455,7 +1454,7 @@ class argument_parser { this->add_optional_argument("input", "i") .required() .nargs(1) - .action(ap::action::check_file_exists_action()) + .action(ap::action::check_file_exists()) .help("Input file path"); break; @@ -1467,7 +1466,7 @@ class argument_parser { this->add_optional_argument("input", "i") .required() .nargs(ap::nargs::at_least(1)) - .action(ap::action::check_file_exists_action()) + .action(ap::action::check_file_exists()) .help("Input files paths"); break;