Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

- [x] `validate()` — post-parse validation hook on the parsed struct
- [ ] `visit_subcommand()` — visitor dispatch over matched subcommands
- [ ] `range` / `choice` — built-in validation constraints
- [ ] pipeline `action` types — validate action types and show clear error messages
- [x] `range` / `choice` — built-in validation constraints
- [x] pipeline `action` types — validate action types and show clear error messages
- [x] env fallback — populate unprovided options from environment variables
- [ ] completion — shell completion script generation
- [x] short cluster / attached value — `-xvf`, `-ofile`
- [ ] `nargs::rest` — consume all remaining tokens as values (`min == max == -1`), disabling further option/subcommand detection for external-command style tails
43 changes: 41 additions & 2 deletions docs/docs/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,47 @@ cli::ActionCtx<int> ctx{.arg = std::ref(storage)};

## Defining Custom Pipeline Steps

A pipeline step is a `constexpr`-constructible struct with an `operator()` and
three type-level traits. cli20 provides two deduction helpers —
For one-off transformations, use `cli::action::custom<fn>`. It wraps a
`constexpr` callable as a non-terminal action step:

```cpp
constexpr auto jobs_or_nproc =
[](cli::ActionCtx<void> ctx,
cli::ActionResult<std::optional<std::string_view>> input)
-> cli::ActionResult<int> {
if (!input.have_value()) return cli::fail<int>(input.error);

if (input.value.has_value()) {
return cli::conversion::integer<int>.invoke(
ctx, cli::ok(std::string_view(*input.value)));
}

return cli::ok(4); // stand-in for a runtime nproc query
};

cli::Arg<"jobs", 'j',
cli::action::custom<jobs_or_nproc>
| cli::validation::range<1, 64>
| cli::pack::set_once,
cli::nargs::zero_or_one>
jobs{{.help = "Parallel job count", .default_value = 1}};
```

The callable must have this shape:

```cpp
ActionResult<U> fn(ActionCtx<void> ctx, ActionResult<T> input);
```

`T` is the current pipeline value and `U` becomes the next pipeline value. Since
`action::custom` is non-terminal, it does not store anything by itself; it must
eventually be followed by a pack action such as `pack::set_once` or
`pack::push`.

Use a reusable struct when the action is part of your API, needs template
parameters, or should be documented/tested independently. A pipeline step is a
`constexpr`-constructible struct with an `operator()` and three type-level
traits. cli20 provides two deduction helpers —
`cli::deduce_accepts_input` and `cli::deduce_after_type` — that derive two of
those traits from `operator()`, so you only write what is unique to your step.

Expand Down
4 changes: 3 additions & 1 deletion docs/docs/examples/advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ This is the reference example for explicit action-pipeline composition. Use it
when the sugar aliases are close but not quite enough.

It is the page to open when you need to understand the lower-level building
blocks behind cli20's higher-level aliases.
blocks behind cli20's higher-level aliases. It also demonstrates
`cli::action::custom<fn>` for a one-off lambda-based action step.

## Source

Expand Down Expand Up @@ -66,4 +67,5 @@ Things to try:
## Related Examples

- [Validation](/docs/examples/validation)
- [Custom Action](/docs/examples/custom-action)
- [Bind](/docs/examples/bind)
4 changes: 2 additions & 2 deletions docs/docs/examples/compiler.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
sidebar_position: 8
title: 7. Compiler Wrapper
sidebar_position: 9
title: 9. Compiler Wrapper
---

import {ExampleSource} from '@site/src/components/ExampleDoc';
Expand Down
65 changes: 65 additions & 0 deletions docs/docs/examples/custom-action.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
sidebar_position: 7
title: 7. Custom Action
---

import {ExampleSource} from '@site/src/components/ExampleDoc';
import {ExamplePlayground} from '@site/src/components/ExamplePlayground';

# Custom Action

This example shows the full struct-based form for defining a reusable action
pipeline step. Use this pattern when a conversion or validation should become
part of your program's reusable vocabulary.

For one-off transformations, prefer `cli::action::custom<fn>` as shown in the
[Advanced](./advanced) example. For reusable actions, define a struct with:

- `accepts_input<Input>`
- `after_type<Input>`
- `storage_type<Input>`
- `operator()(ActionCtx<void>, ActionResult<Input>)`

## Source

<ExampleSource
example={{
sourcePath: 'examples/custom_action.cc',
}}
/>

## What This Example Shows

The example defines `HHMMConversion`, a custom conversion from `HH:MM` strings
to a `MinutesSinceMidnight` value. The action is then wrapped as:

```cpp
inline constexpr auto hhmm = cli::Action<HHMMConversion{}>{};
```

and reused by two options:

```cpp
cli::Arg<"start", 's', hhmm | cli::pack::set_once> start;
cli::Arg<"end", 'e', hhmm | cli::pack::set_once> end;
```

## Try It

<ExamplePlayground
name="custom_action"
defaultArgs="--start 09:30 --end 17:45"
/>

Things to try:

- **Happy path** — `--start 09:30 --end 17:45`
- **Invalid format** — `--start 9am --end 17:45`
- **Out of range** — `--start 24:00 --end 17:45`
- **Missing required option** — `--start 09:30`
- **Help** — `-h` or `--help`

## Related Examples

- [Advanced](./advanced)
- [Validation](./validation)
4 changes: 2 additions & 2 deletions docs/docs/examples/distribution-and-platform.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
sidebar_position: 9
title: 8. Distribution and Platform Examples
sidebar_position: 10
title: 10. Distribution and Platform Examples
---

import {ExampleSource} from '@site/src/components/ExampleDoc';
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/examples/env-fallback.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
sidebar_position: 10
title: 9. Env Fallback
sidebar_position: 11
title: 11. Env Fallback
---

import {ExampleSource} from '@site/src/components/ExampleDoc';
Expand Down
69 changes: 69 additions & 0 deletions docs/docs/examples/git.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
sidebar_position: 8
title: 8. Git
---

import {ExampleSource} from '@site/src/components/ExampleDoc';
import {ExamplePlayground} from '@site/src/components/ExamplePlayground';

# Git

This example mirrors the core behavior of clap's `git.rs` example using cli20's
typed subcommand model. It demonstrates a command-heavy CLI with nested
subcommands and optional positional interpretation.

## Source

<ExampleSource
example={{
sourcePath: 'examples/git.cc',
}}
/>

## What This Example Shows

The top-level command provides:

- `clone <REMOTE>`
- `diff [base] [head] [path] [--color[=WHEN]]`
- `push <REMOTE>`
- `add <PATH>...`
- `stash`, with nested `push`, `pop`, and `apply`

The `diff` command follows the clap example's shifting behavior:

- no positional args: `stage..worktree`
- one positional arg: treat it as `path`
- two positional args: treat them as `base` and `path`
- three positional args: treat them as `base`, `head`, and `path`

The clap example also allows external subcommands. cli20 does not currently
model external subcommands, so this example focuses on the declared command
tree.

## Try It

<ExamplePlayground
name="git"
defaultArgs="diff main feature src/lib.cc --color=always"
/>

Things to try:

- **Clone** — `clone https://example.com/repo.git`
- **Diff defaults** — `diff`
- **Diff path only** — `diff src/main.cc`
- **Diff base/path** — `diff main src/main.cc`
- **Diff full form** — `diff main feature src/main.cc --color=always`
- **Push** — `push origin`
- **Add paths** — `add src/main.cc include/cli/action.hh`
- **Default stash push** — `stash -m wip`
- **Explicit stash push** — `stash push -m wip`
- **Stash pop** — `stash pop stash@{0}`
- **Stash apply** — `stash apply stash@{1}`
- **Help** — `--help`, `diff --help`, or `stash --help`

## Related Examples

- [Subcommands](./subcommand)
- [Advanced](./advanced)
18 changes: 11 additions & 7 deletions docs/docs/examples/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ Each one is small enough to copy into a project, but specific enough to answer
The smallest useful typed-schema CLI: help, a flag, an option, and a required positional.
2. [Subcommands](./subcommand)
Nested commands with their own help output and behavior.
3. [Validation](./validation)
3. [Validation](./validation)
Parse-time checks for numbers, names, and filesystem arguments.
4. [Bind](./bind)
4. [Bind](./bind)
Write values directly into existing variables.
5. [Callback Entry Point](./callback-entry-point)
5. [Callback Entry Point](./callback-entry-point)
Fill application state during parsing, then call the real entry point.
6. [Advanced](./advanced)
6. [Advanced](./advanced)
Lower-level composition with explicit conversion, validation, and packing steps.
7. [Compiler Wrapper](./compiler)
7. [Custom Action](./custom-action)
Full struct-based action definition with `accepts_input`, `after_type`, and `storage_type`.
8. [Git](./git)
A command-heavy CLI inspired by clap's git example, including nested subcommands.
9. [Compiler Wrapper](./compiler)
Short-cluster flags, negatable key-value options, and `std::map` storage with `insert_or_assign`.
8. [Distribution and Platform Examples](./distribution-and-platform)
10. [Distribution and Platform Examples](./distribution-and-platform)
Single-header usage, C++20 modules, and the Windows Unicode sample used by CI.
9. [Env Fallback](./env-fallback)
11. [Env Fallback](./env-fallback)
Populate options from environment variables with the same conversion and validation pipeline.

## Building
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,6 @@ The pipeline is split into three conceptual layers:
| Packing | `cli::pack::*` | Store the final value into the field |

Use the sugar API for the common case and drop down to `Arg` only when you need stronger validation or custom conversion behavior.

For one-off lambda-based steps and reusable struct-based actions, see
[Actions & Pipelines: Defining Custom Pipeline Steps](./actions#defining-custom-pipeline-steps).
31 changes: 10 additions & 21 deletions examples/advanced.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,16 @@ auto nproc_dummy() -> int {
return 4;
}

struct JobsOrNproc {
template <class Input>
static constexpr bool accepts_input =
cli::deduce_accepts_input<JobsOrNproc, Input>;

template <class Input>
using after_type = cli::deduce_after_type<JobsOrNproc, Input>;

template <class Prev>
using storage_type = void;

auto operator()(cli::ActionCtx<void> ctx,
cli::ActionResult<std::optional<std::string_view>> input) const
-> cli::ActionResult<int> {
if (!input.have_value()) return cli::propagate<int>(input);
if (input.value.has_value()) {
return cli::conversion::integer<int>.invoke(
ctx, cli::ActionResult<std::string_view>::ok(input.value.value()));
}
return cli::ActionResult<int>::ok(nproc_dummy());
constexpr auto jobs_or_nproc =
[](cli::ActionCtx<void> ctx,
cli::ActionResult<std::optional<std::string_view>> input)
-> cli::ActionResult<int> {
if (!input.have_value()) return cli::fail<int>(input.error);
if (input.value.has_value()) {
return cli::conversion::integer<int>.invoke(
ctx, cli::ActionResult<std::string_view>::ok(input.value.value()));
}
return cli::ActionResult<int>::ok(nproc_dummy());
};

struct Args {
Expand All @@ -52,7 +41,7 @@ struct Args {
config{{.help = "Configuration file", .presence = cli::required}};

cli::Arg<"jobs", 'j',
cli::Action<JobsOrNproc{}>{} | cli::validation::range<1, 64> |
cli::action::custom<jobs_or_nproc> | cli::validation::range<1, 64> |
cli::pack::set_once,
cli::nargs::zero_or_one>
jobs_arg{{.help = "Parallel job count (default 1; -j uses nproc_dummy())",
Expand Down
Loading
Loading