Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Command-line argument parser for C++20
- [Common Parameters](/docs/tutorial.md#common-parameters)
- [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments)
- [Default Arguments](/docs/tutorial.md#default-arguments)
- [Argument Groups](/docs/tutorial.md#argument-groups)
- [Parsing Arguments](/docs/tutorial.md#parsing-arguments)
- [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules)
- [Compound Arguments](/docs/tutorial.md#compound-arguments)
Expand Down
2 changes: 1 addition & 1 deletion cpp-ap-demo
173 changes: 138 additions & 35 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line)
- [Predefined Parameter Values](#predefined-parameter-values)
- [Default Arguments](#default-arguments)
- [Argument Groups](#argument-groups)
Comment thread
SpectraL519 marked this conversation as resolved.
- [Parsing Arguments](#parsing-arguments)
- [Basic Argument Parsing Rules](#basic-argument-parsing-rules)
- [Compound Arguments](#compound-arguments)
Expand Down Expand Up @@ -118,21 +119,26 @@ If you do not use CMake you can dowload the desired [library release](https://gi

To use the argument parser in your code you need to use the `ap::argument_parser` class.

The parameters you can specify for a parser's instance are:

- The program's name, version and description - used in the parser's configuration output (`std::cout << parser`).
- Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages.
- [Arguments](#adding-arguments) - specify the values/options accepted by the program.
- [The unknown argument flags handling policy](#4-unknown-argument-flag-handling).

```cpp
ap::argument_parser parser;
parser.program_name("Name of the program")
.program_version("alhpa")
ap::argument_parser parser("program");
parser.program_version("alhpa")
.program_description("Description of the program")
.verbose();
```

> [!IMPORTANT]
>
> - When creating an argument parser instance, you must provide a program name to the constructor.
>
> The program name given to the parser cannot contain whitespace characters.
>
> - Additional parameters you can specify for a parser's instance incldue:
> - The program's version and description - used in the parser's configuration output (`std::cout << parser`).
> - Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages.
> - [Arguments](#adding-arguments) - specify the values/options accepted by the program.
> - [Argument Groups](#argument-groups) - organize related optional arguments into sections and optionally enforce usage rules.
> - [The unknown argument flags handling policy](#4-unknown-argument-flag-handling).

> [!TIP]
>
> You can specify the program version using a string (like in the example above) or using the `ap::version` structure:
Expand Down Expand Up @@ -256,8 +262,8 @@ parser.add_positional_argument<std::size_t>("number", "n")
By default all arguments are visible, but this can be modified using the `hidden(bool)` setter as follows:

```cpp
parser.program_name("hidden-test")
.program_description("A simple program")
ap::argument_parser("hidden-test")
parser.program_description("A simple test program for argument hiding")
.default_arguments(ap::default_argument::o_help);

parser.add_optional_argument("hidden")
Expand All @@ -272,7 +278,7 @@ parser.try_parse_args(argc, argv);
> ./hidden-test --help
Program: hidden-test

A simple program
A simple test program for argument hiding

Optional arguments:

Expand Down Expand Up @@ -445,9 +451,8 @@ The `nargs` parameter can be set as:
Consider a simple example:

```cpp
ap::argument_parser parser;
parser.program_name("run-script")
.default_arguments(ap::default_argument::o_help);
ap::argument_parser parser("run-script");
parser.default_arguments(ap::default_argument::o_help);

parser.add_positional_argument("script")
.help("The name of the script to run");
Expand Down Expand Up @@ -807,6 +812,106 @@ parser.default_arguments(<args>);
<br/>
<br/>

## Argument Groups

Argument groups provide a way to organize related optional arguments into logical sections. They make the command-line interface easier to read in help messages, and can enforce rules such as **mutual exclusivity** or **required usage**.

By default, every parser comes with two predefined groups:

- **Positional Arguments** – contains all arguments added via `add_positional_argument`.
- **Optional Arguments** – contains all arguments added via `add_optional_argument` or `add_flag` without explicitly specifying an argument group.

User-defined groups can only contain optional arguments (including flags). This allows you to structure your command-line interface into meaningful sections such as "Input Options", "Output Options", or "Debug Settings".


### Creating New Groups

A new group can be created by calling the `add_group` method of an argument parser:

```cpp
ap::argument_parser parser("myprog");
auto& out_opts = parser.add_group("Output Options");
```

The group’s name will appear as a dedicated section in the help message and arguments added to this group will be listed under `Output Options` instead of the default `Optional Arguments` section.

> [!NOTE]
>
> If a group has no visible arguments, it will not be included in the parser's help message output at all.

### Adding Arguments to Groups

Arguments are added to a group by passing the group reference as the first parameter to the `add_optional_argument` and `add_flag` functions:

```cpp
parser.add_optional_argument(out_opts, "output", "o")
.nargs(1)
.help("Print output to the given file");

parser.add_flag(out_opts, "print", "p")
.help("Print output to the console");
```

### Group Attributes

User-defined groups can be configured with special attributes that change how the parser enforces their usage:

- `required()` – at least one argument from the group must be provided by the user, otherwise parsing will fail.
- `mutually_exclusive()` – at most one argument from the group can be provided; using more than one at the same time results in an error.

Both attributes are **off by default**, and they can be combined (e.g., a group can require that exactly one argument is chosen).

```cpp
auto& out_opts = parser.add_group("Output Options")
.required() // at least one option is required
.mutually_exclusive(); // but at most one can be chosen
```

### Complete Example

Below is a small program that demonstrates how to use a mutually exclusive group of required arguments:

```cpp
#include <ap/argument_parser.hpp>

int main(int argc, char* argv[]) {
ap::argument_parser parser("myprog");
parser.default_arguments(ap::default_argument::o_help);

// create the argument group
auto& out_opts = parser.add_group("Output Options")
.required()
.mutually_exclusive();

// add arguments to the custom group
parser.add_optional_argument(out_opts, "output", "o")
.nargs(1)
.help("Print output to a given file");

parser.add_flag(out_opts, "print", "p")
.help("Print output to the console");

parser.try_parse_args(argc, argv);

return 0;
}
```

When invoked with the `--help` flag, the above program produces a help message that clearly shows the group and its rules:

```
Program: myprog

Output Options: (required, mutually exclusive)

--output, -o : Print output to a given file
--print, -p : Print output to the console
```

<br/>
<br/>
<br/>

## Parsing Arguments

To parse the command-line arguments use the `void argument_parser::parse_args(const AR& argv)` method, where `AR` must be a type that satisfies `std::ranges::range` and its value type is convertible to `std::string`.
Expand Down Expand Up @@ -845,19 +950,18 @@ The simple example below demonstrates how (in terms of the program's structure)

int main(int argc, char* argv[]) {
// create the parser class instance
ap::argument_parser parser;
ap::argument_parser parser("some-program");

// define the parser's attributes
parser.program_name("some-program")
.program_description("The program does something with command-line arguments");
// define the parser's attributes and default arguments
parser.program_version({0u, 0u, 0u})
.program_description("The program does something with command-line arguments")
.default_arguments(ap::default_argument::o_help);

// define the program arguments
parser.add_positional_argument("positional").help("A positional argument");
parser.add_optional_argument("optional", "o").help("An optional argument");
parser.add_flag("flag", "f").help("A boolean flag");

parser.default_arguments(ap::default_argument::o_help);

// parse command-line arguments
parser.try_parse_args(argc, argv);

Expand Down Expand Up @@ -1009,10 +1113,9 @@ This behavior can be modified using the `unknown_arguments_policy` method of the
#include <ap/argument_parser.hpp>

int main(int argc, char* argv[]) {
ap::argument_parser parser;
ap::argument_parser parser("unknown-policy-test");

parser.program_name("test")
.program_description("A simple test program")
parser.program_description("A simple test program for unknwon argument handling policies")
.default_arguments(ap::default_argument::o_help)
// set the unknown argument flags handling policy
.unknown_arguments_policy(ap::unknown_policy::<policy>);
Expand All @@ -1031,12 +1134,12 @@ int main(int argc, char* argv[]) {
The available policies are:
- `ap::unknown_policy::fail` (default) - throws an exception if an unknown argument flag is encountered:

```bash
> ./test --known --unknown
```txt
> ./unknown-policy-test --known --unknown
[ap::error] Unknown argument [--unknown].
Program: test
Program: unknown-policy-test

A simple test program
A simple test program for unknwon argument handling policies

Optional arguments:

Expand All @@ -1046,23 +1149,23 @@ The available policies are:

- `ap::unknown_policy::warn` - prints a warning message to the standard error stream and continues parsing the remaining arguments:

```bash
> ./test --known --unknown
```txt
> ./unknown-policy-test --known --unknown
[ap::warning] Unknown argument '--unknown' will be ignored.
known =
```

- `ap::unknown_policy::ignore` - ignores unknown argument flags and continues parsing the remaining arguments:

```shell
./test --known --unknown
```txt
./unknown-policy-test --known --unknown
known =
```

- `ap::unknown_policy::as_values` - treats unknown argument flags as values:

```shell
> ./test --known --unknown
```txt
> ./unknown-policy-test --known --unknown
known = --unknown
```

Expand Down
2 changes: 2 additions & 0 deletions include/ap/argument.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ class argument : public detail::argument_base {
return *this;
}

// argument& in_group()
Comment thread
SpectraL519 marked this conversation as resolved.
Outdated

#ifdef AP_TESTING
friend struct ::ap_testing::argument_test_fixture;
#endif
Expand Down
108 changes: 108 additions & 0 deletions include/ap/argument_group.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2023-2025 Jakub Musiał
// This file is part of the CPP-AP project (https://github.com/SpectraL519/cpp-ap).
// Licensed under the MIT License. See the LICENSE file in the project root for full license information.

/// @file ap/argument_group.hpp

#pragma once

#include "detail/argument_base.hpp"

#include <memory>

namespace ap {

/**
* @brief Represents a group of arguments.
*
* Groups allow arguments to be organized under a dedicated section in the parser's help message.
*
* A group can be marked as:
* - required: **at least one** argument from the group must be used in the command-line
* - mutually exclusive: **at most one** argument from the group can be used in the command-line
*
* @note - This class is not intended to be constructed directly, but rather through the `add_group` method of @ref ap::argument_parser.
* @note - User defined groups may contain only optional arguments (and flags).
*
* Example usage:
* @code{.cpp}
* ap::argument_parser parser("myprog");
* auto& out_opts = parser.add_group("Output Options").mutually_exclusive();
*
* group.add_optional_argument(out_opts, "output", "o")
* .nargs(1)
* .help("Print output to the given file");
*
* group.add_optional_argument<ap::none_type>(out_opts, "print", "p")
* .help("Print output to the console");
* @endcode
* Here `out_opts` is a mutually exclusive group, so using both arguments at the same time would cause an error.
*/
class argument_group {
public:
argument_group() = delete;

/**
* @brief Set the `required` attribute of the group.
*
* - If set to true, the parser will require at least one argument from the group to be used in the command-line.
* - If no arguments from the group are used, an exception will be thrown.
* - Argument groups are NOT required by default.
*
* @param r The value to set for the attribute (default: true).
* @return Reference to the group instance.
*/
argument_group& required(const bool r = true) noexcept {
this->_required = r;
return *this;
}

/**
* @brief Set the `mutually_exclusive` attribute of the group.
*
* - If set to true, the parser will allow at most one argument from the group to be used in the command-line.
* - If more than one argument from the group is used, an exception will be thrown.
* - Argument groups are NOT mutually exclusive by default.
*
* @param me The value to set for the attribute (default: true).
* @return Reference to the group instance.
*/
argument_group& mutually_exclusive(const bool me = true) noexcept {
this->_mutually_exclusive = me;
return *this;
}

friend class argument_parser;

private:
using arg_ptr_t = std::shared_ptr<detail::argument_base>; ///< The argument pointer type alias.
using arg_ptr_vec_t = std::vector<arg_ptr_t>; ///< The argument pointer list type alias.

/**
* @brief Factory method to create an argument group.
* @param parser The owning parser.
* @param name Name of the group.
*/
static std::unique_ptr<argument_group> create(argument_parser& parser, std::string_view name) {
return std::unique_ptr<argument_group>(new argument_group(parser, name));
}

/// Construct a new argument group with the given name.
argument_group(argument_parser& parser, const std::string_view name)
: _parser(&parser), _name(name) {}

/// Add a new argument to this group (called internally by parser).
void _add_argument(arg_ptr_t arg) noexcept {
this->_arguments.emplace_back(std::move(arg));
}

argument_parser* _parser; ///< Pointer to the owning parser.
std::string _name; ///< Name of the group (used in help output).
arg_ptr_vec_t _arguments; ///< A list of arguments that belong to this group.

bool _required : 1 = false; ///< The required attribute value (default: false).
bool _mutually_exclusive : 1 =
false; ///< The mutually exclusive attribute value (default: false).
};

} // namespace ap
Loading