Skip to content

Commit 101fc33

Browse files
authored
YT-CPPAP-51: Improve string value handling and argument flag detection
- Aligned argument value setting to use direct `value_type` object initialization from `std::string` values if possible (arguments, the value type of which cannot be constructed from `std::string` will use the `istream>>` operator as in previous versions) NOTE: This fixes the issue, where passing a mulit-word optional argument's value, e.g. `./program --option "single value with spaces"` would result in storing only the first word of the value ("single" in this case) - Altered the behavior of unknown argument flag handling: a `parsing_failre` exception is thrown instead of handling it as a value argument token - Added the `AP_UNKNOWN_FLAGS_AS_VALUES` option support: using this option changes the unknown argument flag handling strategy back to treating it as a value - Modified the test building to compile each test file into a separate executable registered with CTest - Corrected the argument value type requirements description in the tutorial document
1 parent beed365 commit 101fc33

31 files changed

+408
-248
lines changed

.github/workflows/clang.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ jobs:
2929
cmake -B build -DBUILD_TESTS=ON -DCMAKE_CXX_COMPILER=$CXX -DCMAKE_C_COMPILER=$CC
3030
continue-on-error: false
3131

32-
- name: Build test executable
32+
- name: Build tests
3333
run: |
34-
cd build && make -j 4
34+
cmake --build build/ -j 4
3535
continue-on-error: false
3636

3737
- name: Run tests
3838
run: |
39-
./build/tests/run
40-
continue-on-error: false
39+
ctest --test-dir build/tests/ -V

.github/workflows/gpp.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ jobs:
2929
cmake -B build -DBUILD_TESTS=ON -DCMAKE_CXX_COMPILER=$CXX -DCMAKE_C_COMPILER=$CC
3030
continue-on-error: false
3131

32-
- name: Build test executable
32+
- name: Build tests
3333
run: |
34-
cd build && make -j 4
34+
cmake --build build/ -j 4
3535
continue-on-error: false
3636

3737
- name: Run tests
3838
run: |
39-
./build/tests/run
39+
ctest --test-dir build/tests/ -V

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# build files
55
*build*/
66
*.exe
7+
Testing/
78

89
# documentation files
910
/documentation

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ else()
77
endif()
88

99
project(cpp-ap
10-
VERSION 2.3.1
10+
VERSION 2.4.0
1111
DESCRIPTION "Command-line argument parser for C++20"
1212
HOMEPAGE_URL "https://github.com/SpectraL519/cpp-ap"
1313
LANGUAGES CXX

Doxyfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ PROJECT_NAME = CPP-AP
4848
# could be handy for archiving the generated documentation or if some version
4949
# control system is used.
5050

51-
PROJECT_NUMBER = 2.3.1
51+
PROJECT_NUMBER = 2.4.0
5252

5353
# Using the PROJECT_BRIEF tag one can provide an optional one line description
5454
# for a project that appears at the top of each page and should give viewer a

docs/dev_notes.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,24 @@
1212

1313
```shell
1414
cmake -B build -DBUILD_TESTS=ON
15-
cd build
16-
make # -j <n>
15+
cmake --build build/ # -j <njobs>
1716
```
1817

19-
This will build the test executable `run` in the `<project-root>/build/tests` directory.
18+
This will build each test file as a separate executable in the `build/tests/` directory.
2019

2120
### Run the tests
2221

22+
You can run tests from each test file separately with:
23+
2324
```shell
24-
cd build
25-
./tests/run # -ts=<test-suite-name>
25+
./build/tests/<test-name>
2626
```
2727

28-
> [!NOTE]
29-
>
30-
> Test suites in the project have the same names as the files they're in except for the `test_extarnal_libs_config.cpp` file which defines the `test_doctest_config` test suite.
28+
To execute all tests at once run:
29+
30+
```shell
31+
ctest --test-dir build/tests/ # -V (to capture output from each test executable)
32+
```
3133

3234
<br />
3335
<br />

docs/tutorial.md

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,19 @@ or
106106
parser.add_<positional/optional>_argument<value_type>("argument", "a");
107107
```
108108

109-
> [!NOTE]
109+
> [!IMPORTANT]
110110
>
111111
> The library supports any argument value types which meet the following requirements:
112112
>
113-
> - The `std::ostream& operator<<` is overloaded for the value type
114-
> - The value type has a copy constructor and an assignment operator
113+
> - The type is [constructible from](https://en.cppreference.com/w/cpp/concepts/constructible_from) `const std::string&` or the stream extraction operator - `std::istream& operator>>` is defined for the type.
114+
>
115+
> **IMPORTANT:** The argument parser will always use direct initialization from `std::string` and will use the extraction operator only if an argument's value type cannot be initialized from `std::string`.
116+
>
117+
> - The type satisfies the [`std::semiregular`](https://en.cppreference.com/w/cpp/concepts/semiregular.html) concept - is default initializable and copyable.
115118
116-
> [!IMPORTANT]
119+
> [!NOTE]
117120
>
118-
> If the `value_type` is not provided, `std::string` will be used.
121+
> The default value type of any argument is `std::string`.
119122
120123
You can also add boolean flags:
121124

@@ -633,6 +636,35 @@ The `argument_parser` class also defines the `void parse_args(int argc, char* ar
633636
> parse_args(std::span(argv + 1, argc - 1));
634637
> ```
635638
639+
> [!WARNING]
640+
>
641+
> By default the `argument_parser` class treats *all\** command-line arguments beggining with a `--` or `-` prefix as optional argument flags and if the flag's value does not match any of the specified arguments, then such flag is considered *unknown* and an exception will be thrown.
642+
>
643+
> *\** If a command-line argument begins with a flag prefix, but contains whitespaces (e.g. `"--flag value"`), then it is treated as a value and not a flag.
644+
>
645+
> This behavior can be altered so that the unknown argument flags will be treated as values, not flags.
646+
>
647+
> Example:
648+
> ```cpp
649+
> parser.add_optional_argument("option", "o");
650+
> parser.try_parse_args(argc, argv);
651+
> std::cout << "option: " << parser.value("option");
652+
>
653+
> /*
654+
> ./program --option --unknown-flag
655+
> option: --unknown-flag
656+
> ```
657+
>
658+
> To do this add the following in you `CMakeLists.txt` file:
659+
> ```cmake
660+
> target_compile_definitions(cpp-ap-2 PRIVATE AP_UNKNOWN_FLAGS_AS_VALUES)
661+
> ```
662+
> or simply add:
663+
> ```cpp
664+
> #define AP_UNKNOWN_FLAGS_AS_VALUES
665+
> ```
666+
> before the `#include <ap/argument_parser.hpp>` statement.
667+
636668
> [!TIP]
637669
>
638670
> The `parse_args` function may throw an `ap::argument_parser_exception` (specifically the `ap::parsing_failure` derived exception) if the provided command-line arguments do not match the expected configuration. To simplify error handling, the `argument_parser` class provides `try_parse_args` methods, which automatically catch these exceptions, print the error message, and exit with a failure status.
@@ -766,7 +798,7 @@ int main(int argc, char* argv[]) {
766798
767799
> [!IMPORTANT]
768800
>
769-
> The parser behaviour depends on the argument definitions. The argument parameters are described int the [Argument parameters](#argument-parameters) section.
801+
> The parser's behavior depends on the argument definitions - see [Argument Parameters](#argument-parameters) section.
770802
771803
<br/>
772804
<br/>
@@ -778,14 +810,14 @@ You can retrieve the argument's value with:
778810
779811
```cpp
780812
(const) auto value = parser.value<value_type>("argument_name"); // (1)
781-
(const) auto value = parser.value_or<value_type>("argument_name", default_value); // (2)
813+
(const) auto value = parser.value_or<value_type>("argument_name", fallback_value); // (2)
782814
```
783815
784816
1. This will return the value parsed for the given argument.
785817
786818
For optional arguments this will return the argument's predefined value if no value has been parsed. Additionaly, if more than one value has been parsed for an optional argument, this function will return the first parsed value.
787819
788-
2. When a value has been parsed for the argument, the behaviour is the same as in case **(1)**. Otherwise, this will return `value_type{std::forward<U>(default_value)}` (where `U` is the deducted type of `default_value`), if:
820+
2. When a value has been parsed for the argument, the behavior is the same as in case **(1)**. Otherwise, this will return `value_type{std::forward<U>(fallback_value)}` (where `U` is the deducted type of `fallback_value`), if:
789821
790822
- There is no value parsed for a positional argument
791823
- There is no parsed values and no predefined values for an optional arrument

include/ap/argument/optional.hpp

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,10 @@ class optional : public detail::argument_base {
207207

208208
/**
209209
* @param verbose The verbosity mode value.
210-
* @param flag_char The character used for the argument flag prefix.
211210
* @return An argument descriptor object for the argument.
212211
*/
213-
[[nodiscard]] detail::argument_descriptor desc(const bool verbose, const char flag_char)
214-
const noexcept override {
215-
detail::argument_descriptor desc(this->_name.str(flag_char), this->_help_msg);
212+
[[nodiscard]] detail::argument_descriptor desc(const bool verbose) const noexcept override {
213+
detail::argument_descriptor desc(this->_name.str(), this->_help_msg);
216214

217215
if (not verbose)
218216
return desc;
@@ -265,8 +263,13 @@ class optional : public detail::argument_base {
265263
throw parsing_failure::invalid_nvalues(this->_name, std::weak_ordering::greater);
266264

267265
value_type value;
268-
if (not (std::istringstream(str_value) >> value))
269-
throw parsing_failure::invalid_value(this->_name, str_value);
266+
if constexpr (detail::c_trivially_readable<value_type>) {
267+
value = value_type(str_value);
268+
}
269+
else {
270+
if (not (std::istringstream(str_value) >> value))
271+
throw parsing_failure::invalid_value(this->_name, str_value);
272+
}
270273

271274
if (not detail::is_valid_choice(value, this->_choices))
272275
throw parsing_failure::invalid_choice(this->_name, str_value);

include/ap/argument/positional.hpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,9 @@ class positional : public detail::argument_base {
156156

157157
/**
158158
* @param verbose The verbosity mode value.
159-
* @param flag_char The character used for the argument flag prefix.
160159
* @return An argument descriptor object for the argument.
161160
*/
162-
[[nodiscard]] detail::argument_descriptor desc(
163-
const bool verbose, [[maybe_unused]] const char flag_char
164-
) const noexcept override {
161+
[[nodiscard]] detail::argument_descriptor desc(const bool verbose) const noexcept override {
165162
detail::argument_descriptor desc(this->_name.str(), this->_help_msg);
166163

167164
if (not verbose)
@@ -210,8 +207,13 @@ class positional : public detail::argument_base {
210207
throw parsing_failure::value_already_set(this->_name);
211208

212209
value_type value;
213-
if (not (std::istringstream(str_value) >> value))
214-
throw parsing_failure::invalid_value(this->_name, str_value);
210+
if constexpr (detail::c_trivially_readable<value_type>) {
211+
value = value_type(str_value);
212+
}
213+
else {
214+
if (not (std::istringstream(str_value) >> value))
215+
throw parsing_failure::invalid_value(this->_name, str_value);
216+
}
215217

216218
if (not detail::is_valid_choice(value, this->_choices))
217219
throw parsing_failure::invalid_choice(this->_name, str_value);

0 commit comments

Comments
 (0)