diff --git a/docs/docs/examples/advanced.mdx b/docs/docs/examples/advanced.mdx
index d704e5f..7db1be6 100644
--- a/docs/docs/examples/advanced.mdx
+++ b/docs/docs/examples/advanced.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 7
-title: 6. Advanced
+sidebar_position: 8
+title: 7. Advanced
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/bind.mdx b/docs/docs/examples/bind.mdx
index d3c19fa..9e26586 100644
--- a/docs/docs/examples/bind.mdx
+++ b/docs/docs/examples/bind.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 5
-title: 4. Bind
+sidebar_position: 6
+title: 5. Bind
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/callback-entry-point.mdx b/docs/docs/examples/callback-entry-point.mdx
index d320d4d..2d22e90 100644
--- a/docs/docs/examples/callback-entry-point.mdx
+++ b/docs/docs/examples/callback-entry-point.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 6
-title: 5. Callback Entry Point
+sidebar_position: 7
+title: 6. Callback Entry Point
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/compiler.mdx b/docs/docs/examples/compiler.mdx
index f3b8273..a06aeae 100644
--- a/docs/docs/examples/compiler.mdx
+++ b/docs/docs/examples/compiler.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 9
-title: 9. Compiler Wrapper
+sidebar_position: 11
+title: 10. Compiler Wrapper
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/custom-action.mdx b/docs/docs/examples/custom-action.mdx
index 705941c..568b9df 100644
--- a/docs/docs/examples/custom-action.mdx
+++ b/docs/docs/examples/custom-action.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 7
-title: 7. Custom Action
+sidebar_position: 9
+title: 8. Custom Action
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/distribution-and-platform.mdx b/docs/docs/examples/distribution-and-platform.mdx
index aac0f6b..b3aba3c 100644
--- a/docs/docs/examples/distribution-and-platform.mdx
+++ b/docs/docs/examples/distribution-and-platform.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 10
-title: 10. Distribution and Platform Examples
+sidebar_position: 12
+title: 11. Distribution and Platform Examples
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/env-fallback.mdx b/docs/docs/examples/env-fallback.mdx
index d849a20..24baeda 100644
--- a/docs/docs/examples/env-fallback.mdx
+++ b/docs/docs/examples/env-fallback.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 11
-title: 11. Env Fallback
+sidebar_position: 13
+title: 12. Env Fallback
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/git.mdx b/docs/docs/examples/git.mdx
index 2ee0bac..2469e29 100644
--- a/docs/docs/examples/git.mdx
+++ b/docs/docs/examples/git.mdx
@@ -1,6 +1,6 @@
---
-sidebar_position: 8
-title: 8. Git
+sidebar_position: 10
+title: 9. Git
---
import {ExampleSource} from '@site/src/components/ExampleDoc';
diff --git a/docs/docs/examples/index.mdx b/docs/docs/examples/index.mdx
index cd0c31a..ad97cfc 100644
--- a/docs/docs/examples/index.mdx
+++ b/docs/docs/examples/index.mdx
@@ -18,21 +18,23 @@ Each one is small enough to copy into a project, but specific enough to answer
Nested commands with their own help output and behavior.
3. [Validation](./validation)
Parse-time checks for numbers, names, and filesystem arguments.
-4. [Bind](./bind)
+4. [Relations](./relations)
+ Schema-level relationships such as groups, conflicts, and depends_on.
+5. [Bind](./bind)
Write values directly into existing variables.
-5. [Callback Entry Point](./callback-entry-point)
+6. [Callback Entry Point](./callback-entry-point)
Fill application state during parsing, then call the real entry point.
-6. [Advanced](./advanced)
+7. [Advanced](./advanced)
Lower-level composition with explicit conversion, validation, and packing steps.
-7. [Custom Action](./custom-action)
+8. [Custom Action](./custom-action)
Full struct-based action definition with `accepts_input`, `after_type`, and `storage_type`.
-8. [Git](./git)
+9. [Git](./git)
A command-heavy CLI inspired by clap's git example, including nested subcommands.
-9. [Compiler Wrapper](./compiler)
+10. [Compiler Wrapper](./compiler)
Short-cluster flags, negatable key-value options, and `std::map` storage with `insert_or_assign`.
-10. [Distribution and Platform Examples](./distribution-and-platform)
+11. [Distribution and Platform Examples](./distribution-and-platform)
Single-header usage, C++20 modules, and the Windows Unicode sample used by CI.
-11. [Env Fallback](./env-fallback)
+12. [Env Fallback](./env-fallback)
Populate options from environment variables with the same conversion and validation pipeline.
## Building
diff --git a/docs/docs/examples/relations.mdx b/docs/docs/examples/relations.mdx
new file mode 100644
index 0000000..a4c98f4
--- /dev/null
+++ b/docs/docs/examples/relations.mdx
@@ -0,0 +1,53 @@
+---
+sidebar_position: 5
+title: 4. Relations
+---
+
+import {ExampleSource} from '@site/src/components/ExampleDoc';
+
+# Relations
+
+This example shows schema-level relationships between options. Use this pattern
+when the rule can be described from occurrence alone and should also appear in
+generated help. Relation metadata is rendered inline on affected options, while
+named groups get their own `Option groups:` entry.
+
+## Source
+
+
+
+## What This Example Shows
+
+The example defines three static relations:
+
+- `user` and `password` are a named `legacy_auth` group and must be provided together.
+- `json` conflicts with `markdown`.
+- the named `legacy_auth` group depends on `profile`.
+
+The parser enforces these rules after normal option parsing and before the
+program uses the parsed values.
+
+## Try It
+
+```bash
+./build/examples/relations --json
+./build/examples/relations --user alice --password secret --profile prod
+./build/examples/relations --markdown --profile prod
+```
+
+Failure cases:
+
+- `--user alice` fails because `password` is missing from the `legacy_auth` group.
+- `--json --markdown` fails because the two output formats conflict.
+- `--user alice --password secret` fails because `legacy_auth` depends on `profile`.
+- `--help` shows relation metadata next to `--user`, `--password`, `--json`,
+ `--markdown`, and `--profile`.
+
+## Related Examples
+
+- [Validation](/docs/examples/validation)
+- [Advanced](/docs/examples/advanced)
diff --git a/docs/docs/relations.mdx b/docs/docs/relations.mdx
new file mode 100644
index 0000000..0ca0535
--- /dev/null
+++ b/docs/docs/relations.mdx
@@ -0,0 +1,103 @@
+---
+sidebar_position: 4
+title: Relations
+---
+
+# Relations
+
+Use `static constexpr auto relations` when a relationship between fields should
+be part of the typed schema, generated help, and parse-time validation. The
+help generator renders short relation metadata inline on affected options, and
+named groups in an `Option groups:` section.
+
+This is intentionally metadata, not a replacement for `constraints()`.
+Relations cover simple occurrence-based rules that are independent of runtime
+values. Keep `constraints()` for value-dependent logic such as "`--type foo`
+requires `--foo-config`".
+
+## API
+
+```cpp
+struct Args {
+ cli::StringOption<"user"> user;
+ cli::StringOption<"password"> password;
+ cli::Flag<"json"> json;
+ cli::Flag<"markdown"> markdown;
+ cli::StringOption<"profile"> profile;
+
+ static constexpr auto relations = cli::relations(
+ cli::group({.name = "legacy_auth", .members = {"user", "password"}}),
+ cli::conflicts("json", "markdown"),
+ cli::depends_on("legacy_auth", "profile"));
+};
+```
+
+Names are written without the command-line prefix: use `"json"`, not
+`"--json"`.
+
+## Relation Types
+
+- `cli::group({.name = "...", .members = {...}})` requires all members in the
+ named group to be provided together, or none of them.
+- `cli::conflicts("a", "b")` rejects input when both operands are provided.
+- `cli::depends_on("a", "b")` rejects input when `a` is provided but `b` is not.
+
+All checks are based on `provided()`: defaults do not count as CLI occurrence.
+
+## Named Groups
+
+`group` creates a named group that can be reused as an operand in another
+relation:
+
+```cpp
+static constexpr auto relations = cli::relations(
+ cli::group({.name = "legacy_auth", .members = {"user", "password"}}),
+ cli::depends_on("legacy_auth", "profile"));
+```
+
+This means `--user` and `--password` must appear together, and that legacy auth
+requires `--profile`.
+
+The generated help shows the rule where users look for it:
+
+```text
+--user User name [requires: --profile]
+--password Password [requires: --profile]
+--profile Profile [required by: legacy_auth]
+
+Option groups:
+ legacy_auth:
+ --user, --password must be used together
+ requires: --profile
+```
+
+## Well-Formed Schema Rules
+
+The relation schema is checked at compile time as part of `ArgumentSpec`.
+
+- Every field or group name used by a relation must exist.
+- A `group` needs a non-empty unique name and at least two members.
+- A group name must not collide with an option or command name.
+- Each field may participate in only one direct relation.
+- `conflicts("a", "a")` and `depends_on("a", "a")` are invalid.
+
+These rules are deliberately strict. They keep help text and parse-time
+validation unambiguous.
+
+## Relation Metadata vs `constraints()`
+
+Use relations when the rule is static and occurrence-based:
+
+- `--json` conflicts with `--markdown`
+- `legacy_auth` depends on `--profile`
+- `--user` and `--password` must be provided together
+
+Use `constraints()` when the rule depends on parsed values:
+
+- `--type s3` requires `--bucket`
+- `--mode fast` rejects `--exact`
+- one numeric option must be greater than another
+
+## Example
+
+See [Relations Example](./examples/relations) for a complete program.
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 3a3b5b2..2942081 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -13,6 +13,7 @@
"@docusaurus/plugin-google-gtag": "^3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@mdx-js/react": "^3.0.0",
+ "ansi_up": "^6.0.6",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
@@ -6796,6 +6797,15 @@
"algoliasearch": ">= 3.1 < 6"
}
},
+ "node_modules/ansi_up": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.6.tgz",
+ "integrity": "sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
diff --git a/docs/package.json b/docs/package.json
index 94ea4af..4c04551 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -22,6 +22,7 @@
"@docusaurus/plugin-google-gtag": "^3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@mdx-js/react": "^3.0.0",
+ "ansi_up": "^6.0.6",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
diff --git a/docs/sidebars.ts b/docs/sidebars.ts
index ba892d3..2880a57 100644
--- a/docs/sidebars.ts
+++ b/docs/sidebars.ts
@@ -5,6 +5,7 @@ const sidebars: SidebarsConfig = {
'intro',
'tutorial',
'actions',
+ 'relations',
{
type: 'category',
label: 'Examples',
diff --git a/docs/src/components/ExamplePlayground.tsx b/docs/src/components/ExamplePlayground.tsx
index 44c27e1..0bd2ca2 100644
--- a/docs/src/components/ExamplePlayground.tsx
+++ b/docs/src/components/ExamplePlayground.tsx
@@ -1,5 +1,13 @@
import React, {useEffect, useRef, useState} from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
+import {AnsiUp} from 'ansi_up';
+
+const ansi = new AnsiUp();
+ansi.escape_html = true;
+
+function ansiToHtml(text: string): string {
+ return ansi.ansi_to_html(text);
+}
function splitArgs(input: string): string[] {
const args: string[] = [];
@@ -361,7 +369,11 @@ export function ExamplePlayground({
)}
{entry.output && (
- {entry.output}
+
)}
))}
diff --git a/examples/relations.cc b/examples/relations.cc
new file mode 100644
index 0000000..e660600
--- /dev/null
+++ b/examples/relations.cc
@@ -0,0 +1,36 @@
+#include
+
+#include "cli/argument.hh"
+#include "cli/parser.hh"
+
+struct Args {
+ cli::Description description{
+ "Relation metadata example covering groups, conflicts, and depends_on."};
+ cli::Help<> help;
+
+ cli::StringOption<"user"> user{{.help = "Legacy auth user name"}};
+ cli::StringOption<"password"> password{{.help = "Legacy auth password"}};
+
+ cli::Flag<"json"> json{{.help = "Write JSON output"}};
+ cli::Flag<"markdown"> markdown{{.help = "Write Markdown output"}};
+
+ cli::StringOption<"profile"> profile{{.help = "Deployment profile"}};
+
+ static constexpr auto relations = cli::relations(
+ cli::group({.name = "legacy_auth", .members = {"user", "password"}}),
+ cli::conflicts("json", "markdown"),
+ cli::depends_on("legacy_auth", "profile"));
+};
+
+auto main(int argc, char* argv[]) -> int {
+ const auto args = cli::parse_or_exit(argc, argv);
+
+ std::cout << "format: " << (args.json ? "json" : "markdown") << '\n';
+ if (args.user) {
+ std::cout << "legacy auth: " << *args.user << '\n';
+ }
+ if (args.profile) {
+ std::cout << "profile: " << *args.profile << '\n';
+ }
+ return 0;
+}
diff --git a/flake.nix b/flake.nix
index c25bf3f..518f6ef 100644
--- a/flake.nix
+++ b/flake.nix
@@ -102,7 +102,7 @@
npmDeps = pkgs.fetchNpmDeps {
name = "cli20-docs-0.0.0-npm-deps";
src = ./docs;
- hash = "sha256-DOI2KP2qq8zbUjX3Kktgfe16n567pNkkGz5ABGSgSdo=";
+ hash = "sha256-ntnQTTWdih6fLx6QLqarpJQ6pIOdDWRxmysDQQVWswQ=";
};
nativeBuildInputs = [
pkgs.emscripten
diff --git a/include/cli/argument.hh b/include/cli/argument.hh
index 19d1990..be8dee3 100644
--- a/include/cli/argument.hh
+++ b/include/cli/argument.hh
@@ -9,332 +9,14 @@
#include
#include "cli/action.hh"
+#include "cli/argument_base.hh"
+#include "cli/argument_validation.hh"
#include "cli/meta.hh"
+#include "cli/relation.hh"
#include "cli/string_literal.hh"
namespace cli {
-/** @brief Base tag for all members that may appear in an argument specification
- * struct. */
-struct SpecMemberTag {};
-
-/** @brief Tag base class for command-line option/flag fields. */
-struct OptionTag : SpecMemberTag {};
-
-/** @brief Tag base class for positional argument fields. */
-struct PositionalTag : SpecMemberTag {};
-
-/** @brief Tag base class for subcommand fields. */
-struct CommandTag : SpecMemberTag {};
-
-/** @brief Tag base class for the `Description` field that provides the CLI's
- * help text. */
-struct DescriptionTag : SpecMemberTag {};
-
-/**
- * @brief Specifies the minimum and maximum number of values an option or
- * positional accepts.
- *
- * The default `{0, -1}` is equivalent to `nargs::zero_or_more`.
- *
- * **Valid ranges:**
- * - `min >= 0`
- * - `max >= 1`, or `max == -1` which means *unlimited* (no upper bound)
- * - `min <= max` (when `max != -1`)
- *
- * Prefer the predefined constants in the `cli::nargs` namespace over
- * constructing `Nargs` directly.
- */
-struct Nargs {
- int min = 0; ///< Minimum number of values (>= 0).
- int max = -1; ///< Maximum number of values; -1 means unlimited.
-};
-
-/**
- * @brief Controls whether an option or positional argument is mandatory.
- *
- * When `required`, the parser returns `ErrorCode::missing_required` if the
- * argument is absent. When `optional` (the default), absence is allowed.
- */
-enum class Presence { required, optional };
-
-/** @brief Convenience constant for `Presence::required`. */
-inline constexpr auto required = Presence::required;
-
-/** @brief Convenience constant for `Presence::optional`. */
-inline constexpr auto optional = Presence::optional;
-
-/**
- * @brief Predefined `Nargs` constants for common value-count patterns.
- */
-namespace nargs {
-
-/** @brief Accepts no values; used for boolean flags. `{0, 0}` */
-inline constexpr Nargs none{.min = 0, .max = 0};
-
-/** @brief Accepts exactly one value. `{1, 1}` */
-inline constexpr Nargs one{.min = 1, .max = 1};
-
-/** @brief Accepts zero or one value. `{0, 1}` */
-inline constexpr Nargs zero_or_one{.min = 0, .max = 1};
-
-/** @brief Accepts zero or more values. `{0, -1}` */
-inline constexpr Nargs zero_or_more{.min = 0, .max = -1};
-
-/** @brief Accepts one or more values. `{1, -1}` */
-inline constexpr Nargs one_or_more{.min = 1, .max = -1};
-
-/**
- * @brief Accepts exactly `N` values.
- * @tparam N The required number of values.
- */
-template
-inline constexpr Nargs exactly{.min = N, .max = N};
-
-/**
- * @brief Accepts between `Min` and `Max` values (inclusive).
- * @tparam Min Minimum number of values.
- * @tparam Max Maximum number of values.
- */
-template
-inline constexpr Nargs between{.min = Min, .max = Max};
-
-} // namespace nargs
-
-namespace detail {
-
-/**
- * @brief Checks that every member of aggregate type `T` derives from
- * `SpecMemberTag`.
- *
- * @tparam T The argument specification type to validate.
- * @return `true` if all members inherit from `SpecMemberTag`.
- */
-template
-consteval auto members_are_derived_from_valid_cli_class() -> bool {
- return [](std::type_identity>) consteval
- -> auto {
- return (std::derived_from, SpecMemberTag> && ...);
- }(std::type_identity<
- std::remove_cvref_t()))>>{});
-}
-
-/**
- * @brief Checks that all `OptionTag` members of `T` have distinct long names.
- *
- * @tparam T The argument specification type to validate.
- * @return `true` if no two options share the same long name.
- */
-template
-consteval auto options_have_unique_long_name() -> bool {
- std::vector names;
- [&](std::type_identity>) consteval
- -> auto {
- (
- [&]() consteval -> auto {
- if constexpr (std::derived_from,
- OptionTag>) {
- names.push_back(std::remove_cvref_t::name.view());
- }
- }(),
- ...);
- }(std::type_identity<
- std::remove_cvref_t()))>>{});
- std::ranges::sort(names);
- return std::ranges::adjacent_find(names) == names.end();
-}
-
-/**
- * @brief Checks that all `OptionTag` members of `T` have distinct short names.
- *
- * Options with no short name (`'\0'`) are excluded from the uniqueness check.
- *
- * @tparam T The argument specification type to validate.
- * @return `true` if no two options share the same non-null short name.
- */
-template
-consteval auto options_have_unique_short_name() -> bool {
- std::vector names;
- [&](std::type_identity>) consteval
- -> auto {
- (
- [&]() consteval -> auto {
- if constexpr (std::derived_from,
- OptionTag>) {
- if constexpr (std::remove_cvref_t::short_name != '\0') {
- names.push_back(std::remove_cvref_t::short_name);
- }
- }
- }(),
- ...);
- }(std::type_identity<
- std::remove_cvref_t()))>>{});
- std::ranges::sort(names);
- return std::ranges::adjacent_find(names) == names.end();
-}
-
-/**
- * @brief Checks that all `CommandTag` members of `T` have distinct names.
- *
- * @tparam T The argument specification type to validate.
- * @return `true` if no two subcommands share the same name.
- */
-template
-consteval auto commands_have_unique_long_name() -> bool {
- std::vector names;
- [&](std::type_identity>) consteval
- -> auto {
- (
- [&]() consteval -> auto {
- if constexpr (std::derived_from,
- CommandTag>) {
- names.push_back(std::remove_cvref_t::name.view());
- }
- }(),
- ...);
- }(std::type_identity<
- std::remove_cvref_t()))>>{});
- std::ranges::sort(names);
- return std::ranges::adjacent_find(names) == names.end();
-}
-
-/**
- * @brief Checks that any variadic positional appears after all fixed
- * positionals.
- *
- * A variadic positional is one where `nargs.min != nargs.max`. The constraint
- * ensures that token dispatch is unambiguous.
- *
- * @tparam T The argument specification type to validate.
- * @return `true` if no fixed positional follows a variadic positional.
- */
-template
-consteval auto positionals_have_variadic_at_end() {
- bool found_variadic = false;
- bool found_positional_after_variadic = false;
- [&](std::type_identity>) consteval
- -> auto {
- (
- [&]() consteval -> auto {
- if constexpr (std::derived_from,
- PositionalTag>) {
- if (found_variadic) {
- found_positional_after_variadic = true;
- }
- if (std::remove_cvref_t::nargs.max !=
- std::remove_cvref_t::nargs.min) {
- found_variadic = true;
- }
- }
- }(),
- ...);
- }(std::type_identity<
- std::remove_cvref_t()))>>{});
- return !found_positional_after_variadic;
-}
-
-/**
- * @brief Validates the syntax of a long option name at compile time.
- *
- * Rules:
- * - Must be non-empty.
- * - Must start with a lowercase ASCII letter.
- * - May contain lowercase letters, digits, and single hyphens.
- * - Must not contain consecutive hyphens or end with a hyphen.
- *
- * @tparam Name The option name to validate (without the `--` prefix).
- * @return `true` if `Name` is a valid long option name.
- */
-template
-[[nodiscard]]
-constexpr auto is_valid_long_option_name() noexcept -> bool {
- constexpr auto size = Name.size();
-
- if constexpr (size == 0) {
- return false;
- }
- auto is_alpha = [](char c) constexpr -> bool {
- return ('a' <= c && c <= 'z');
- };
- auto is_digit = [](char c) constexpr -> bool { return '0' <= c && c <= '9'; };
- auto is_alnum = [&](char c) constexpr -> bool {
- return is_alpha(c) || is_digit(c);
- };
-
- if (!is_alpha(Name[0])) {
- return false;
- }
- bool previous_is_hyphen = false;
- for (std::size_t i = 1; i < size; ++i) {
- const char c = Name[i];
- if (c == '-') {
- if (previous_is_hyphen) {
- return false;
- }
- previous_is_hyphen = true;
- continue;
- }
- if (!is_alnum(c)) {
- return false;
- }
- previous_is_hyphen = false;
- }
-
- return !previous_is_hyphen;
-}
-
-/**
- * @brief Validates a short option name character at compile time.
- *
- * Accepts ASCII letters (upper or lower case) or `'\0'` (meaning no short name).
- *
- * @param Name The short option character to validate.
- * @return `true` if `Name` is a valid short option name.
- */
-constexpr auto is_valid_short_option_name(char Name) noexcept -> bool {
- return ('a' <= Name && Name <= 'z') || ('A' <= Name && Name <= 'Z') ||
- Name == '\0';
-}
-
-/**
- * @brief Validates the syntax of a subcommand name at compile time.
- *
- * Applies the same rules as `is_valid_long_option_name`.
- *
- * @tparam Name The command name to validate.
- * @return `true` if `Name` is a valid command name.
- */
-template
-[[nodiscard]]
-consteval auto is_valid_command_name() noexcept {
- return is_valid_long_option_name();
-}
-
-/**
- * @brief Validates that a `Nargs` value satisfies the basic range constraints.
- *
- * Checks that `min >= 0` and that `max` is either -1 (unlimited) or
- * satisfies `max >= min`. The `ArgImpl` and `PositionalImpl` template
- * `requires` clauses enforce these same constraints at the point of
- * instantiation.
- *
- * @tparam NargsValue The `Nargs` value to validate.
- * @return `true` if `NargsValue` is a well-formed nargs specification.
- */
-template
-[[nodiscard]]
-consteval auto is_valid_nargs() noexcept -> bool {
- if (NargsValue.min < 0) {
- return false;
- }
- if (NargsValue.max != -1 && NargsValue.max < NargsValue.min) {
- return false;
- }
- return true;
-}
-
-} // namespace detail
-
/**
* @brief Concept that validates an aggregate type as a well-formed CLI argument
* specification.
@@ -358,6 +40,7 @@ concept ArgumentSpec = requires {
requires detail::options_have_unique_short_name();
requires detail::commands_have_unique_long_name();
requires detail::positionals_have_variadic_at_end();
+ requires detail::relations_are_well_formed();
};
template
@@ -388,6 +71,9 @@ struct ArgParameter {
on_parse{}; ///< Callback invoked once after parsing completes.
std::string_view env{}; ///< Environment variable name to fall back to when
///< the option is absent from the command line.
+ bool hidden{}; ///< Whether this argument is omitted from generated help.
+ std::string_view
+ deprecated{}; ///< Optional deprecation message shown in generated help.
};
namespace detail {
@@ -461,6 +147,15 @@ struct ArgImpl : public OptionTag {
[[nodiscard]] auto help_text() const -> const std::string_view {
return param_.help;
}
+ [[nodiscard]] auto hidden() const -> bool { return param_.hidden; }
+ [[nodiscard]] auto deprecated() const -> std::string_view {
+ return param_.deprecated;
+ }
+ [[nodiscard]] auto presence() const -> Presence { return param_.presence; }
+ [[nodiscard]] auto env() const -> std::string_view { return param_.env; }
+ [[nodiscard]] auto default_value() const -> const std::optional& {
+ return param_.default_value;
+ }
protected:
static constexpr bool entry_is_optional =
@@ -613,6 +308,15 @@ struct PositionalImpl : public PositionalTag {
[[nodiscard]] auto help_text() const -> const std::string_view {
return param_.help;
}
+ [[nodiscard]] auto hidden() const -> bool { return param_.hidden; }
+ [[nodiscard]] auto deprecated() const -> std::string_view {
+ return param_.deprecated;
+ }
+ [[nodiscard]] auto presence() const -> Presence { return param_.presence; }
+ [[nodiscard]] auto env() const -> std::string_view { return param_.env; }
+ [[nodiscard]] auto default_value() const -> const std::optional& {
+ return param_.default_value;
+ }
protected:
[[nodiscard]] auto get_param() const -> const ArgParameter& {
@@ -678,7 +382,10 @@ struct Description : public std::string, DescriptionTag {};
*/
struct CommandParameter {
std::string_view
- help{}; ///< Brief description shown in the parent command's help.
+ help{}; ///< Brief description shown in the parent command's help.
+ bool hidden{}; ///< Whether this command is omitted from generated help.
+ std::string_view
+ deprecated{}; ///< Optional deprecation message shown in generated help.
};
/**
@@ -699,17 +406,26 @@ struct Command : public T, public CommandTag {
using argument_type = T;
Command() = default;
- Command(CommandParameter param) : help_(param.help) {}
+ Command(CommandParameter param)
+ : help_(param.help),
+ hidden_(param.hidden),
+ deprecated_(param.deprecated) {}
static constexpr auto name = Name;
static constexpr auto command_name() -> std::string_view {
return Name.view();
}
[[nodiscard]] auto provided() const -> bool { return provided_; }
[[nodiscard]] auto help_text() const -> std::string_view { return help_; }
+ [[nodiscard]] auto hidden() const -> bool { return hidden_; }
+ [[nodiscard]] auto deprecated() const -> std::string_view {
+ return deprecated_;
+ }
auto mark_provided() -> void { provided_ = true; }
private:
std::string_view help_{};
+ bool hidden_{};
+ std::string_view deprecated_{};
bool provided_{};
};
diff --git a/include/cli/argument_base.hh b/include/cli/argument_base.hh
new file mode 100644
index 0000000..06d5484
--- /dev/null
+++ b/include/cli/argument_base.hh
@@ -0,0 +1,69 @@
+#pragma once
+
+namespace cli {
+
+/** @brief Base tag for all members that may appear in an argument specification
+ * struct. */
+struct SpecMemberTag {};
+
+/** @brief Tag base class for command-line option/flag fields. */
+struct OptionTag : SpecMemberTag {};
+
+/** @brief Tag base class for positional argument fields. */
+struct PositionalTag : SpecMemberTag {};
+
+/** @brief Tag base class for subcommand fields. */
+struct CommandTag : SpecMemberTag {};
+
+/** @brief Tag base class for the `Description` field that provides the CLI's
+ * help text. */
+struct DescriptionTag : SpecMemberTag {};
+
+/**
+ * @brief Specifies the minimum and maximum number of values an option or
+ * positional accepts.
+ *
+ * The default `{0, -1}` is equivalent to `nargs::zero_or_more`.
+ */
+struct Nargs {
+ int min = 0; ///< Minimum number of values (>= 0).
+ int max = -1; ///< Maximum number of values; -1 means unlimited.
+};
+
+/**
+ * @brief Controls whether an option or positional argument is mandatory.
+ */
+enum class Presence { required, optional };
+
+/** @brief Convenience constant for `Presence::required`. */
+inline constexpr auto required = Presence::required;
+
+/** @brief Convenience constant for `Presence::optional`. */
+inline constexpr auto optional = Presence::optional;
+
+namespace nargs {
+
+/** @brief Accepts no values; used for boolean flags. `{0, 0}` */
+inline constexpr Nargs none{.min = 0, .max = 0};
+
+/** @brief Accepts exactly one value. `{1, 1}` */
+inline constexpr Nargs one{.min = 1, .max = 1};
+
+/** @brief Accepts zero or one value. `{0, 1}` */
+inline constexpr Nargs zero_or_one{.min = 0, .max = 1};
+
+/** @brief Accepts zero or more values. `{0, -1}` */
+inline constexpr Nargs zero_or_more{.min = 0, .max = -1};
+
+/** @brief Accepts one or more values. `{1, -1}` */
+inline constexpr Nargs one_or_more{.min = 1, .max = -1};
+
+template
+inline constexpr Nargs exactly{.min = N, .max = N};
+
+template
+inline constexpr Nargs between{.min = Min, .max = Max};
+
+} // namespace nargs
+
+} // namespace cli
diff --git a/include/cli/argument_validation.hh b/include/cli/argument_validation.hh
new file mode 100644
index 0000000..3cadc73
--- /dev/null
+++ b/include/cli/argument_validation.hh
@@ -0,0 +1,315 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "cli/argument_base.hh"
+#include "cli/meta.hh"
+#include "cli/relation.hh"
+#include "cli/string_literal.hh"
+
+namespace cli::detail {
+
+template
+consteval auto members_are_derived_from_valid_cli_class() -> bool {
+ return [](std::type_identity>) consteval
+ -> auto {
+ return (std::derived_from, SpecMemberTag> && ...);
+ }(std::type_identity<
+ std::remove_cvref_t()))>>{});
+}
+
+template
+consteval auto options_have_unique_long_name() -> bool {
+ std::vector names;
+ [&](std::type_identity>) consteval
+ -> auto {
+ (
+ [&]() consteval -> auto {
+ if constexpr (std::derived_from,
+ OptionTag>) {
+ names.push_back(std::remove_cvref_t::name.view());
+ }
+ }(),
+ ...);
+ }(std::type_identity<
+ std::remove_cvref_t()))>>{});
+ std::ranges::sort(names);
+ return std::ranges::adjacent_find(names) == names.end();
+}
+
+template
+consteval auto options_have_unique_short_name() -> bool {
+ std::vector names;
+ [&](std::type_identity>) consteval
+ -> auto {
+ (
+ [&]() consteval -> auto {
+ if constexpr (std::derived_from,
+ OptionTag>) {
+ if constexpr (std::remove_cvref_t::short_name != '\0') {
+ names.push_back(std::remove_cvref_t::short_name);
+ }
+ }
+ }(),
+ ...);
+ }(std::type_identity<
+ std::remove_cvref_t()))>>{});
+ std::ranges::sort(names);
+ return std::ranges::adjacent_find(names) == names.end();
+}
+
+template
+consteval auto commands_have_unique_long_name() -> bool {
+ std::vector names;
+ [&](std::type_identity>) consteval
+ -> auto {
+ (
+ [&]() consteval -> auto {
+ if constexpr (std::derived_from,
+ CommandTag>) {
+ names.push_back(std::remove_cvref_t::name.view());
+ }
+ }(),
+ ...);
+ }(std::type_identity<
+ std::remove_cvref_t()))>>{});
+ std::ranges::sort(names);
+ return std::ranges::adjacent_find(names) == names.end();
+}
+
+template
+consteval auto positionals_have_variadic_at_end() {
+ bool found_variadic = false;
+ bool found_positional_after_variadic = false;
+ [&](std::type_identity>) consteval
+ -> auto {
+ (
+ [&]() consteval -> auto {
+ if constexpr (std::derived_from,
+ PositionalTag>) {
+ if (found_variadic) {
+ found_positional_after_variadic = true;
+ }
+ if (std::remove_cvref_t::nargs.max !=
+ std::remove_cvref_t::nargs.min) {
+ found_variadic = true;
+ }
+ }
+ }(),
+ ...);
+ }(std::type_identity<
+ std::remove_cvref_t()))>>{});
+ return !found_positional_after_variadic;
+}
+
+template
+concept HasRelations = requires {
+ { T::relations } -> RelationSetLike;
+};
+
+template
+consteval auto relation_target_name() -> std::string_view {
+ using F = std::remove_cvref_t;
+ if constexpr (std::derived_from ||
+ std::derived_from) {
+ return F::name.view();
+ } else {
+ return {};
+ }
+}
+
+template
+consteval auto named_relation_targets() {
+ return [&](std::type_identity>) consteval
+ -> auto {
+ return std::array{
+ relation_target_name()...};
+ }(std::type_identity<
+ std::remove_cvref_t()))>>{});
+}
+
+template
+consteval auto contains_name(const Array& names, std::string_view name) -> bool {
+ for (auto candidate : names) {
+ if (!candidate.empty() && candidate == name) return true;
+ }
+ return false;
+}
+
+template
+consteval auto group_name_exists_before(std::string_view name) -> bool {
+ if constexpr (I >= Limit) {
+ return false;
+ } else {
+ constexpr auto rel = std::get(T::relations.items);
+ using R = std::remove_cvref_t;
+ if constexpr (std::same_as) {
+ if (rel.name == name) return true;
+ }
+ return group_name_exists_before(name);
+ }
+}
+
+template
+consteval auto group_name_exists(std::string_view name) -> bool {
+ constexpr auto size =
+ std::tuple_size_v>;
+ return group_name_exists_before(name);
+}
+
+template
+consteval auto operand_exists(std::string_view name) -> bool {
+ constexpr auto field_names = named_relation_targets();
+ return contains_name(field_names, name) || group_name_exists(name);
+}
+
+template
+consteval auto relation_membership_count(std::string_view name) -> std::size_t {
+ constexpr auto size =
+ std::tuple_size_v>;
+ if constexpr (I >= size) {
+ return 0;
+ } else {
+ constexpr auto rel = std::get(T::relations.items);
+ using R = std::remove_cvref_t;
+ std::size_t current = 0;
+ if constexpr (std::same_as) {
+ for (auto member : rel.members) {
+ if (member == name) ++current;
+ }
+ } else if constexpr (std::same_as) {
+ if (rel.left == name) ++current;
+ if (rel.right == name) ++current;
+ } else if constexpr (std::same_as) {
+ if (rel.source == name) ++current;
+ if (rel.target == name) ++current;
+ }
+ return current + relation_membership_count(name);
+ }
+}
+
+template
+consteval auto field_relation_memberships_are_unique() -> bool {
+ constexpr auto field_names = named_relation_targets();
+ for (auto name : field_names) {
+ if (!name.empty() && relation_membership_count(name) > 1) {
+ return false;
+ }
+ }
+ return true;
+}
+
+template
+consteval auto relation_items_are_well_formed() -> bool {
+ constexpr auto size =
+ std::tuple_size_v>;
+ if constexpr (I >= size) {
+ return true;
+ } else {
+ constexpr auto rel = std::get(T::relations.items);
+ using R = std::remove_cvref_t;
+ constexpr auto field_names = named_relation_targets();
+ if constexpr (std::same_as) {
+ if (rel.name.empty() || rel.members.size < 2 ||
+ contains_name(field_names, rel.name) ||
+ group_name_exists_before(rel.name)) {
+ return false;
+ }
+ for (auto member : rel.members) {
+ if (!contains_name(field_names, member)) return false;
+ }
+ } else if constexpr (std::same_as) {
+ if (rel.left.empty() || rel.right.empty() || rel.left == rel.right ||
+ !operand_exists(rel.left) || !operand_exists(rel.right)) {
+ return false;
+ }
+ } else if constexpr (std::same_as) {
+ if (rel.source.empty() || rel.target.empty() || rel.source == rel.target ||
+ !operand_exists(rel.source) || !operand_exists(rel.target)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ return relation_items_are_well_formed();
+ }
+}
+
+template
+consteval auto relations_are_well_formed() -> bool {
+ if constexpr (!HasRelations) {
+ return true;
+ } else {
+ return relation_items_are_well_formed() &&
+ field_relation_memberships_are_unique();
+ }
+}
+
+template
+[[nodiscard]]
+constexpr auto is_valid_long_option_name() noexcept -> bool {
+ constexpr auto size = Name.size();
+
+ if constexpr (size == 0) {
+ return false;
+ }
+ auto is_alpha = [](char c) constexpr -> bool {
+ return ('a' <= c && c <= 'z');
+ };
+ auto is_digit = [](char c) constexpr -> bool { return '0' <= c && c <= '9'; };
+ auto is_alnum = [&](char c) constexpr -> bool {
+ return is_alpha(c) || is_digit(c);
+ };
+
+ if (!is_alpha(Name[0])) {
+ return false;
+ }
+ bool previous_is_hyphen = false;
+ for (std::size_t i = 1; i < size; ++i) {
+ const char c = Name[i];
+ if (c == '-') {
+ if (previous_is_hyphen) {
+ return false;
+ }
+ previous_is_hyphen = true;
+ continue;
+ }
+ if (!is_alnum(c)) {
+ return false;
+ }
+ previous_is_hyphen = false;
+ }
+
+ return !previous_is_hyphen;
+}
+
+constexpr auto is_valid_short_option_name(char Name) noexcept -> bool {
+ return ('a' <= Name && Name <= 'z') || ('A' <= Name && Name <= 'Z') ||
+ Name == '\0';
+}
+
+template
+[[nodiscard]]
+consteval auto is_valid_command_name() noexcept {
+ return is_valid_long_option_name();
+}
+
+template
+[[nodiscard]]
+consteval auto is_valid_nargs() noexcept -> bool {
+ if (NargsValue.min < 0) {
+ return false;
+ }
+ if (NargsValue.max != -1 && NargsValue.max < NargsValue.min) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace cli::detail
diff --git a/include/cli/color.hh b/include/cli/color.hh
index 779b2b7..8316e97 100644
--- a/include/cli/color.hh
+++ b/include/cli/color.hh
@@ -13,11 +13,30 @@ namespace cli {
/**
* @brief Controls whether ANSI escape codes are emitted by `format_help()`.
*
- * - `auto_` : emit codes only when stdout is connected to a TTY (default).
+ * - `detect` : emit codes only when stdout is connected to a TTY (default).
* - `never` : always produce plain text.
* - `always` : always emit ANSI codes regardless of terminal type.
*/
-enum class ColorMode { auto_, never, always };
+enum class ColorMode { detect, never, always };
+
+/**
+ * @brief ANSI palette used by generated help output.
+ *
+ * Each field is an ANSI SGR sequence. `format_help(ColorMode::never, ...)`
+ * suppresses these sequences regardless of the palette contents.
+ */
+struct HelpPalette {
+ std::string_view usage{"\033[1m"};
+ std::string_view heading{"\033[1;4;36m"};
+ std::string_view option{"\033[1;32m"};
+ std::string_view metavar{"\033[36m"};
+ std::string_view command{"\033[1;33m"};
+ std::string_view metadata{"\033[2m"};
+ std::string_view group{"\033[1;33m"};
+ std::string_view reset{"\033[0m"};
+};
+
+inline constexpr HelpPalette default_help_palette{};
/**
* @brief Tag type passed to `format_help()` to request recursive sub-command
@@ -46,6 +65,7 @@ namespace detail {
struct AnsiStyle {
/** @brief Whether ANSI codes should be emitted. */
bool enabled;
+ HelpPalette palette{};
/**
* @brief Returns the ANSI bold sequence, or an empty string when disabled.
@@ -72,7 +92,35 @@ struct AnsiStyle {
* @return `"\033[0m"` if enabled, `""` otherwise.
*/
[[nodiscard]] constexpr auto reset() const noexcept -> std::string_view {
- return enabled ? "\033[0m" : "";
+ return enabled ? palette.reset : "";
+ }
+
+ [[nodiscard]] constexpr auto usage() const noexcept -> std::string_view {
+ return enabled ? palette.usage : "";
+ }
+
+ [[nodiscard]] constexpr auto heading() const noexcept -> std::string_view {
+ return enabled ? palette.heading : "";
+ }
+
+ [[nodiscard]] constexpr auto option() const noexcept -> std::string_view {
+ return enabled ? palette.option : "";
+ }
+
+ [[nodiscard]] constexpr auto metavar() const noexcept -> std::string_view {
+ return enabled ? palette.metavar : "";
+ }
+
+ [[nodiscard]] constexpr auto command() const noexcept -> std::string_view {
+ return enabled ? palette.command : "";
+ }
+
+ [[nodiscard]] constexpr auto metadata() const noexcept -> std::string_view {
+ return enabled ? palette.metadata : "";
+ }
+
+ [[nodiscard]] constexpr auto group() const noexcept -> std::string_view {
+ return enabled ? palette.group : "";
}
};
@@ -97,15 +145,17 @@ inline auto is_tty() noexcept -> bool {
*
* - `ColorMode::always` → `AnsiStyle{true}`
* - `ColorMode::never` → `AnsiStyle{false}`
- * - `ColorMode::auto_` → `AnsiStyle{is_tty()}`
+ * - `ColorMode::detect` → `AnsiStyle{is_tty()}`
*
* @param mode The requested color mode.
* @return An `AnsiStyle` with `enabled` set appropriately.
*/
-inline auto resolve_color(ColorMode mode) noexcept -> AnsiStyle {
+inline auto resolve_color(ColorMode mode,
+ HelpPalette palette = default_help_palette) noexcept
+ -> AnsiStyle {
const bool on =
- (mode == ColorMode::always) || (mode == ColorMode::auto_ && is_tty());
- return AnsiStyle{on};
+ (mode == ColorMode::always) || (mode == ColorMode::detect && is_tty());
+ return AnsiStyle{.enabled = on, .palette = palette};
}
} // namespace detail
diff --git a/include/cli/help.hh b/include/cli/help.hh
index cac02e2..a4b9c40 100644
--- a/include/cli/help.hh
+++ b/include/cli/help.hh
@@ -2,6 +2,9 @@
#include
#include
+#include
+#include
+#include
#include
#include
#include
@@ -152,11 +155,11 @@ auto command_usage_suffix(T& value) -> std::string {
[&]() -> auto {
using F = std::remove_cvref_t;
if constexpr (std::derived_from) {
- has_options = true;
+ if (!fields.hidden()) has_options = true;
} else if constexpr (std::derived_from) {
- has_positionals = true;
+ if (!fields.hidden()) has_positionals = true;
} else if constexpr (std::derived_from) {
- has_commands = true;
+ if (!fields.hidden()) has_commands = true;
}
}(),
...);
@@ -211,6 +214,39 @@ auto field_usage_label() -> std::string {
}
}
+template
+[[nodiscard]]
+auto styled_field_usage_label(const AnsiStyle& style) -> std::string {
+ using F = std::remove_cvref_t;
+ const auto reset = style.reset();
+ if constexpr (std::derived_from) {
+ std::string out;
+ if constexpr (F::short_name != '\0') {
+ out += style.option();
+ out += std::format("-{}", F::short_name);
+ out += reset;
+ out += ", ";
+ }
+ out += style.option();
+ out += std::format("--{}", F::name.view());
+ out += reset;
+ if constexpr (!(F::nargs.min == 0 && F::nargs.max == 0)) {
+ out += " ";
+ out += style.metavar();
+ out += metavar_for();
+ out += reset;
+ }
+ return out;
+ } else if constexpr (std::derived_from) {
+ return std::format("{}{}{}", style.metavar(),
+ metavar_for(), reset);
+ } else if constexpr (std::derived_from) {
+ return std::format("{}{}{}", style.command(), F::command_name(), reset);
+ } else {
+ return std::string{};
+ }
+}
+
/**
* @brief Splits a string on newline characters.
*
@@ -339,13 +375,244 @@ auto append_section(std::string& out, std::string_view title,
* @return The formatted row string.
*/
template
-auto render_help_row(Field& field, std::string_view label, std::size_t width)
- -> std::string {
+consteval auto relation_field_name() -> std::string_view {
+ using F = std::remove_cvref_t;
+ if constexpr (std::derived_from ||
+ std::derived_from) {
+ return F::name.view();
+ } else {
+ return {};
+ }
+}
+
+inline auto relation_name_list(NameList<> names) -> std::string {
+ std::string out;
+ for (std::size_t i = 0; i < names.size; ++i) {
+ if (i != 0) out += ", ";
+ out += "--";
+ out += names.values[i];
+ }
+ return out;
+}
+
+template
+constexpr auto relation_group(std::string_view name) -> NameList<> {
+ NameList<> result;
+ if constexpr (detail::HasRelations) {
+ std::apply(
+ [&](auto... rels) -> auto {
+ (
+ [&]() -> auto {
+ using R = std::remove_cvref_t;
+ if constexpr (std::same_as) {
+ if (rels.name == name) result = rels.members;
+ }
+ }(),
+ ...);
+ },
+ T::relations.items);
+ }
+ return result;
+}
+
+template
+constexpr auto relation_operand_contains(std::string_view operand,
+ std::string_view field_name) -> bool {
+ if (operand == field_name) return true;
+ const auto group = relation_group(operand);
+ for (auto member : group) {
+ if (member == field_name) return true;
+ }
+ return false;
+}
+
+template
+auto relation_operand_label(std::string_view operand) -> std::string {
+ if constexpr (detail::HasRelations) {
+ if (!relation_group(operand).empty()) {
+ return std::string(operand);
+ }
+ }
+ return std::format("--{}", operand);
+}
+
+template
+auto relation_help_metadata() -> std::vector {
+ std::vector metadata;
+ constexpr auto field_name = relation_field_name();
+ if constexpr (field_name.empty()) {
+ return metadata;
+ } else if constexpr (detail::HasRelations) {
+ std::apply(
+ [&](auto... rels) -> auto {
+ (
+ [&]() -> auto {
+ using R = std::remove_cvref_t;
+ if constexpr (std::same_as) {
+ (void)field_name;
+ } else if constexpr (std::same_as) {
+ if (relation_operand_contains(rels.left, field_name)) {
+ metadata.emplace_back(std::format(
+ "conflicts: {}", relation_operand_label(rels.right)));
+ }
+ if (relation_operand_contains(rels.right, field_name)) {
+ metadata.emplace_back(std::format(
+ "conflicts: {}", relation_operand_label(rels.left)));
+ }
+ } else if constexpr (std::same_as) {
+ if (relation_operand_contains(rels.source, field_name)) {
+ metadata.emplace_back(std::format(
+ "requires: {}", relation_operand_label(rels.target)));
+ }
+ if (relation_operand_contains(rels.target, field_name)) {
+ metadata.emplace_back(
+ std::format("required by: {}",
+ relation_operand_label(rels.source)));
+ }
+ }
+ }(),
+ ...);
+ },
+ T::relations.items);
+ return metadata;
+ } else {
+ return metadata;
+ }
+}
+
+template
+auto help_metadata(Field& field) -> std::vector {
+ std::vector metadata;
+ if constexpr (std::derived_from, OptionTag> ||
+ std::derived_from, PositionalTag>) {
+ if (field.presence() == Presence::required) {
+ metadata.emplace_back("required");
+ }
+ if (!field.env().empty()) {
+ metadata.emplace_back(std::format("env: {}", field.env()));
+ }
+ if (const auto& default_value = field.default_value()) {
+ using Value = std::remove_cvref_t;
+ if constexpr (requires(std::ostream& os, const Value& value) {
+ os << value;
+ }) {
+ std::ostringstream out;
+ out << *default_value;
+ metadata.emplace_back(std::format("default: {}", out.str()));
+ }
+ }
+ }
+ if (!field.deprecated().empty()) {
+ metadata.emplace_back(std::format("deprecated: {}", field.deprecated()));
+ }
+ auto relation_metadata = relation_help_metadata();
+ metadata.insert(metadata.end(),
+ std::make_move_iterator(relation_metadata.begin()),
+ std::make_move_iterator(relation_metadata.end()));
+ return metadata;
+}
+
+inline auto append_help_metadata(std::string& out,
+ const std::vector& metadata,
+ const AnsiStyle& style) -> void {
+ if (metadata.empty()) {
+ return;
+ }
+ out += style.metadata();
+ out += " [";
+ for (std::size_t i = 0; i < metadata.size(); ++i) {
+ if (i != 0) {
+ out += "] [";
+ }
+ out += metadata[i];
+ }
+ out += "]";
+ out += style.reset();
+}
+
+inline auto append_row_description(std::string& out,
+ std::string_view description,
+ const std::vector& metadata,
+ const AnsiStyle& style, std::size_t indent)
+ -> void {
+ auto lines = split_lines(description);
+ out += lines.front();
+ append_help_metadata(out, metadata, style);
+ out += '\n';
+ for (std::size_t i = 1; i < lines.size(); ++i) {
+ out.append(indent, ' ');
+ out += lines[i];
+ out += '\n';
+ }
+}
+
+template
+auto append_group_requirements(std::string& row, std::string_view group_name)
+ -> void {
+ if constexpr (detail::HasRelations) {
+ std::apply(
+ [&](auto... rels) -> auto {
+ (
+ [&]() -> auto {
+ using R = std::remove_cvref_t;
+ if constexpr (std::same_as) {
+ if (rels.source == group_name) {
+ row += std::format(" requires: {}\n",
+ relation_operand_label(rels.target));
+ }
+ }
+ }(),
+ ...);
+ },
+ T::relations.items);
+ }
+}
+
+template
+auto relation_option_group_row(const GroupRelation& group,
+ const AnsiStyle& style) -> std::string {
+ std::string row = " ";
+ row += style.group();
+ row += group.name;
+ row += style.reset();
+ row += ":\n ";
+ row += relation_name_list(group.members);
+ row += " must be used together\n";
+ append_group_requirements(row, group.name);
+ return row;
+}
+
+template
+auto relation_option_group_rows(const AnsiStyle& style)
+ -> std::vector {
+ std::vector rows;
+ if constexpr (detail::HasRelations) {
+ std::apply(
+ [&](auto... rels) -> auto {
+ (
+ [&]() -> auto {
+ using R = std::remove_cvref_t;
+ if constexpr (std::same_as) {
+ rows.push_back(relation_option_group_row(rels, style));
+ }
+ }(),
+ ...);
+ },
+ T::relations.items);
+ }
+ return rows;
+}
+
+template
+auto render_help_row(Field& field, std::string_view label,
+ std::string_view styled_label, std::size_t width,
+ const AnsiStyle& style) -> std::string {
std::string out = " ";
- out += label;
- if (!field.help_text().empty()) {
+ out += styled_label;
+ const auto metadata = help_metadata(field);
+ if (!field.help_text().empty() || !metadata.empty()) {
out.append(width - label.size() + 2, ' ');
- append_wrapped_description(out, field.help_text(), width + 4);
+ append_row_description(out, field.help_text(), metadata, style, width + 4);
} else {
out += '\n';
}
@@ -408,11 +675,11 @@ auto find_description(T& value) -> std::string_view {
template
auto format_help_impl(T& value, std::string_view program_name,
ColorMode color_mode, bool recurse,
- std::string_view command_path) -> std::string {
- const auto style = resolve_color(color_mode);
- const std::string heading =
- std::string(style.bold()) + std::string(style.underline());
- const std::string option_color = std::string(style.bold());
+ std::string_view command_path,
+ HelpPalette palette = default_help_palette)
+ -> std::string {
+ const auto style = resolve_color(color_mode, palette);
+ const std::string heading = std::string(style.heading());
const std::string reset = std::string(style.reset());
std::string full_program = std::string(program_name);
@@ -421,8 +688,8 @@ auto format_help_impl(T& value, std::string_view program_name,
full_program += command_path;
}
- std::string out =
- std::format("Usage: {}{}\n", full_program, command_usage_suffix(value));
+ std::string out = std::format("{}Usage:{} {}{}\n", style.usage(), reset,
+ full_program, command_usage_suffix(value));
if (const auto description = find_description(value); !description.empty()) {
out += '\n';
@@ -430,9 +697,9 @@ auto format_help_impl(T& value, std::string_view program_name,
out += '\n';
}
- std::vector> option_rows;
- std::vector> positional_rows;
- std::vector> command_rows;
+ std::vector> option_rows;
+ std::vector> positional_rows;
+ std::vector> command_rows;
std::size_t option_width = 0;
std::size_t positional_width = 0;
std::size_t command_width = 0;
@@ -443,18 +710,20 @@ auto format_help_impl(T& value, std::string_view program_name,
[&]() -> auto {
using F = std::remove_cvref_t;
if constexpr (std::derived_from) {
+ if (fields.hidden()) return;
auto label = field_usage_label();
option_width = std::max(option_width, label.size());
- option_rows.emplace_back(std::move(label), fields.help_text());
+ option_rows.emplace_back(std::move(label), std::string{});
} else if constexpr (std::derived_from) {
+ if (fields.hidden()) return;
auto label = field_usage_label();
positional_width = std::max(positional_width, label.size());
- positional_rows.emplace_back(std::move(label),
- fields.help_text());
+ positional_rows.emplace_back(std::move(label), std::string{});
} else if constexpr (std::derived_from) {
+ if (fields.hidden()) return;
auto label = field_usage_label();
command_width = std::max(command_width, label.size());
- command_rows.emplace_back(std::move(label), fields.help_text());
+ command_rows.emplace_back(std::move(label), std::string{});
}
}(),
...);
@@ -463,8 +732,7 @@ auto format_help_impl(T& value, std::string_view program_name,
auto append_rows =
[&](std::string_view title,
- const std::vector>& rows,
- std::size_t width) -> auto {
+ const std::vector>& rows) -> auto {
if (rows.empty()) {
return;
}
@@ -473,23 +741,62 @@ auto format_help_impl(T& value, std::string_view program_name,
out += title;
out += reset;
out += '\n';
- for (const auto& [label, description] : rows) {
- out += " ";
- out += option_color;
- out += label;
- out += reset;
- if (!description.empty()) {
- out.append(width - label.size() + 2, ' ');
- append_wrapped_description(out, description, width + 4);
- } else {
- out += '\n';
- }
+ for (const auto& [label, row] : rows) {
+ (void)label;
+ out += row;
}
};
- append_rows("Options:", option_rows, option_width);
- append_rows("Positional arguments:", positional_rows, positional_width);
- append_rows("Commands:", command_rows, command_width);
+ option_rows.clear();
+ positional_rows.clear();
+ command_rows.clear();
+ std::apply(
+ [&](auto&... fields) -> auto {
+ (
+ [&]() -> auto {
+ using F = std::remove_cvref_t;
+ if constexpr (std::derived_from) {
+ if (fields.hidden()) return;
+ auto label = field_usage_label();
+ auto styled_label = styled_field_usage_label(style);
+ option_rows.emplace_back(
+ label, render_help_row(fields, label, styled_label,
+ option_width, style));
+ } else if constexpr (std::derived_from) {
+ if (fields.hidden()) return;
+ auto label = field_usage_label();
+ auto styled_label = styled_field_usage_label(style);
+ positional_rows.emplace_back(
+ label, render_help_row(fields, label, styled_label,
+ positional_width, style));
+ } else if constexpr (std::derived_from) {
+ if (fields.hidden()) return;
+ auto label = field_usage_label();
+ auto styled_label = styled_field_usage_label