Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d518312
Add strict option parsing diagnostics and runtime validation
carldebilly Mar 1, 2026
1b15d60
Add custom global options parsing and collision validation
carldebilly Mar 1, 2026
38fbe5f
Support response files and case-aware option parsing
carldebilly Mar 1, 2026
805b689
Document strict option parsing and response file support
carldebilly Mar 1, 2026
04b06ce
Disable response-file expansion by default in interactive sessions
carldebilly Mar 1, 2026
9651023
Add integration coverage for option case-sensitivity modes
carldebilly Mar 1, 2026
c6f070e
Address parser edge cases and add parameter-system documentation
carldebilly Mar 1, 2026
74778e8
Add schema-driven option parsing and attribute-based binding
carldebilly Mar 1, 2026
fd7e916
Align help/completion/doc with schema and move option engine internals
carldebilly Mar 1, 2026
86f793a
Document attribute-based option declarations and internal option engi…
carldebilly Mar 1, 2026
602d4b9
Organize parameter API into Repl.Parameters namespace and folders
carldebilly Mar 1, 2026
ee79bd0
Add enum value suggestions for shell option completion
carldebilly Mar 1, 2026
8a847f2
Show global options in help including custom registrations
carldebilly Mar 1, 2026
13d9780
Update docs for Repl.Parameters namespace and global options help
carldebilly Mar 1, 2026
cb9453d
Move documentation contracts to Repl.Documentation namespace
carldebilly Mar 1, 2026
242e0fe
Group shell completion public API under Repl.ShellCompletion namespace
carldebilly Mar 1, 2026
ebb22e1
Move terminal public API to Repl.Terminal namespace
carldebilly Mar 1, 2026
d2bd52c
Move interaction public API to Repl.Interaction namespace
carldebilly Mar 1, 2026
accf31b
Move autocomplete public API to Repl.Autocomplete namespace
carldebilly Mar 1, 2026
7e2154e
Document interaction and terminal public namespaces
carldebilly Mar 1, 2026
0842398
Move ANSI rendering contracts to Repl.Rendering namespace
carldebilly Mar 1, 2026
ecba1fe
Expand shell completion tests for enum/options across all shells
carldebilly Mar 1, 2026
2d2acc5
Address PR review feedback and polish parameter docs
carldebilly Mar 1, 2026
21cada8
Simplify response-file parsing toggle expression
carldebilly Mar 1, 2026
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,11 @@ ws-7c650a64 websocket [::1]:60288 301x31 xterm-256color 1m 34s 1s
- **Hierarchical contexts** (scopes) with validation and navigation results (`NavigateUp`, `NavigateTo`)
- **Routing constraints** (`{id:int}`, `{when:date}`, `{x:guid}`…) plus custom constraints
- **Parsing and binding** for named options, positional args, route values, and injected services
- **Strict option validation by default** (unknown options fail fast; configurable)
- **Response files** with `@file.rsp` expansion for complex invocations
- **Output pipeline** with transformers and aliases
(`--output:<format>`, `--json`, `--yaml`, `--markdown`, …)
- **Extensible global options** via `options.Parsing.AddGlobalOption<T>(...)`
- **Typed result model** (`Results.Ok/Error/Validation/NotFound/Cancelled`, etc.)
- **Protocol passthrough mode** for stdio transports (`AsProtocolPassthrough()`), keeping `stdout` reserved for protocol payloads
- **Typed interactions**: prompts, progress, status, timeouts, cancellation
Expand Down Expand Up @@ -279,6 +282,7 @@ Package details:

- Architecture blueprint: [`docs/architecture.md`](docs/architecture.md)
- Command reference: [`docs/commands.md`](docs/commands.md)
- Parameter system notes: [`docs/parameter-system.md`](docs/parameter-system.md)
- Terminal/session metadata: [`docs/terminal-metadata.md`](docs/terminal-metadata.md)
- Testing toolkit: [`docs/testing-toolkit.md`](docs/testing-toolkit.md)
- Conditional module presence: [`docs/module-presence.md`](docs/module-presence.md)
Expand Down
6 changes: 6 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@
- Transport-native signaling (DTTERM push, Telnet NAWS/TERMINAL-TYPE) is preferred.
- `@@repl:*` control messages and out-of-band metadata are supported extension patterns.

## Related docs

- Command reference: `docs/commands.md`
- Parameter system: `docs/parameter-system.md`
- Shell completion: `docs/shell-completion.md`

## Branching and versioning

- NBGV (`version.json`) drives package/release versioning.
Expand Down
83 changes: 83 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,89 @@ These flags are parsed before route execution:
- `--output:<format>`
- output aliases mapped by `OutputOptions.Aliases` (defaults include `--json`, `--xml`, `--yaml`, `--yml`, `--markdown`)
- `--answer:<name>[=value]` for non-interactive prompt answers
- custom global options registered via `options.Parsing.AddGlobalOption<T>(...)`

Global parsing notes:

- unknown command options are validation errors by default (`options.Parsing.AllowUnknownOptions = false`)
- option name matching is case-sensitive by default (`options.Parsing.OptionCaseSensitivity = CaseSensitive`)
- option value syntaxes accepted by command parsing: `--name value`, `--name=value`, `--name:value`
- use `--` to stop option parsing and force remaining tokens to positional arguments
- response files are supported with `@file.rsp` (enabled by default); nested `@` expansion is not supported

## Declaring command options

Handler parameters can declare explicit option behavior with attributes:

- `[ReplOption]` for named options
- `[ReplArgument]` for positional behavior
- `[ReplValueAlias]` for token-to-value injection
- `[ReplEnumFlag]` on enum members for enum-token aliases

Example:

```csharp
using Repl.Parameters;

app.Map(
"render",
([ReplOption(Aliases = ["-m"])] RenderMode mode = RenderMode.Fast,
[ReplOption(ReverseAliases = ["--no-verbose"])] bool verbose = false) =>
$"{mode}:{verbose}");
```

Root help now includes a dedicated `Global Options:` section with built-ins plus custom options registered through `options.Parsing.AddGlobalOption<T>(...)`.

## Parse diagnostics model

Command option parsing returns structured diagnostics through the internal `OptionParsingResult` model:

- `Diagnostics`: list of `ParseDiagnostic`
- `HasErrors`: true when any diagnostic has `Severity = Error`
- `ParseDiagnostic` fields:
- `Severity`: `Error` or `Warning`
- `Message`: user-facing explanation
- `Token`: source token when available
- `Suggestion`: optional typo hint (for example `--output`)

Runtime behavior:

- when at least one parsing error is present, command execution stops and the first error is rendered as a validation result
- warnings do not block execution

## Response file examples

`@file.rsp` is expanded before command option parsing.

Example file:

```text
--output json
# comments are ignored outside quoted sections
"two words"
```

Command:

```text
myapp echo @args.rsp
```

Notes:

- quotes and escapes are supported by the response-file tokenizer
- a standalone `@` token is treated as a normal positional token
- in interactive sessions, response-file expansion is disabled by default
- response-file paths are read from the local filesystem as provided; treat `@file` input as trusted CLI input

## Supported parameter conversions

Handler parameters support native conversion for:

- `FileInfo` from string path tokens (for example `--path ./file.txt`)
- `DirectoryInfo` from string path tokens (for example `--path ./folder`)

Path existence is not validated at parse time; handlers decide validation policy.

## Ambient commands

Expand Down
108 changes: 108 additions & 0 deletions docs/parameter-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Parameter System

This document describes Repl Toolkit's parameter/option model and key design decisions.

## Goals

- keep app-side declaration simple (handler-first, low ceremony)
- keep runtime parsing strict and predictable by default
- share one option model across parsing, help, completion, and docs

## Current behavior highlights

- unknown command options are validation errors by default
- option value syntaxes: `--name value`, `--name=value`, `--name:value`
- option parsing is case-sensitive by default, configurable via `ParsingOptions.OptionCaseSensitivity`
- response files are supported with `@file.rsp` (non-recursive)
- custom global options can be registered via `ParsingOptions.AddGlobalOption<T>(...)`
- signed numeric literals (`-1`, `-0.5`, `-1e3`) are treated as positional values, not options

## Public declaration API

Application-facing parameter DSL:

- `ReplOptionAttribute`
- canonical `Name`
- explicit `Aliases` (full tokens, for example `-m`, `--mode`)
- explicit `ReverseAliases` (for example `--no-verbose`)
- `Mode` (`OptionOnly`, `ArgumentOnly`, `OptionAndPositional`)
- optional per-parameter `CaseSensitivity`
- optional `Arity`
- `ReplArgumentAttribute`
- optional positional `Position`
- `Mode`
- `ReplValueAliasAttribute`
- maps a token to an injected parameter value (for example `--json` -> `output=json`)
- `ReplEnumFlagAttribute`
- maps enum members to explicit alias tokens

Supporting enums:

- `ReplCaseSensitivity`
- `ReplParameterMode`
- `ReplArity`

These public types live under `Repl.Parameters`.
Typical app code starts with:

```csharp
using Repl.Parameters;
```

## Public namespace map

The public API is grouped by concern:

- `Repl.Parameters` for option/argument declaration attributes
- `Repl.Documentation` for documentation export contracts
- `Repl.ShellCompletion` for shell completion setup/runtime options
- `Repl.Terminal` for terminal metadata/control contracts
- `Repl.Interaction` for prompt/progress/status interaction contracts
- `Repl.Autocomplete` for interactive autocomplete options
- `Repl.Rendering` for ANSI rendering/palette contracts

## Internal architecture boundary

The option engine internals are intentionally not public:

- schema model and runtime parser internals live under `src/Repl.Core/Internal/Options`
- these internals are consumed by command parsing, help rendering, shell completion, and documentation export
- only the declaration DSL above is public for application code

Documentation-export contracts are also separated from the root namespace:

- `DocumentationExportOptions`
- `ReplDocumentationModel`
- `ReplDoc*` records

These types now live under `Repl.Documentation`.

## Help/completion/doc consistency

Command option metadata is generated from one internal schema per route.
This same schema drives:

- runtime parsing and diagnostics
- command help option sections
- shell option completion candidates
- exported documentation option metadata

## System.CommandLine comparison

### Similarities

- modern long-option syntaxes and `--` sentinel semantics
- structured parsing errors and typo suggestions
- explicit aliases and discoverable command surfaces

### Differences

- Repl Toolkit is handler-first and REPL/session-aware by design
- global options are consumed before command routing and can be app-extended
- response-file expansion is disabled by default in interactive sessions
- short-option bundling (`-abc` -> `-a -b -c`) is not enabled implicitly

## Notes

- this document is intentionally focused on parameter-system behavior and tradeoffs
- API-level and phased implementation details remain tracked in active engineering tasks
8 changes: 8 additions & 0 deletions docs/terminal-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

This file is the canonical behavior document for terminal/session metadata in Repl Toolkit.

## Public API Namespace

Terminal control and capability contracts are exposed from `Repl.Terminal`:

```csharp
using Repl.Terminal;
```

## Boundary model (A-D) with real OSI references

| Alias | Real OSI layer(s) | Scope in this project | Notes |
Expand Down
2 changes: 2 additions & 0 deletions docs/testing-toolkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ It is test-framework-agnostic and assertion-library-agnostic.

```csharp
using Repl.Testing;
using Repl.Interaction;
using Repl.Terminal;

await using var host = ReplTestHost.Create(() =>
{
Expand Down
1 change: 1 addition & 0 deletions samples/04-interactive-ops/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Repl.Interaction;
2 changes: 2 additions & 0 deletions samples/05-hosting-remote/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using Repl.Interaction;
global using Repl.Terminal;
1 change: 1 addition & 0 deletions samples/06-testing/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
global using AwesomeAssertions;
global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using Repl.Interaction;
global using Repl;
global using Repl.Testing;
global using System;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Repl;
namespace Repl.Autocomplete;

/// <summary>
/// Configures interactive autocomplete behavior.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Repl;
namespace Repl.Autocomplete;

/// <summary>
/// Interactive autocomplete options.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Repl;
namespace Repl.Autocomplete;

/// <summary>
/// Defines high-level autocomplete interaction style.
Expand Down
52 changes: 47 additions & 5 deletions src/Repl.Core/CoreReplApp.Documentation.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Reflection;
using Repl.Internal.Options;

namespace Repl;

Expand Down Expand Up @@ -150,11 +151,7 @@ private ReplDocCommand BuildDocumentationCommand(RouteDefinition route)
&& parameter.ParameterType != typeof(CancellationToken)
&& !routeParameterNames.Contains(parameter.Name!)
&& !IsFrameworkInjectedParameter(parameter.ParameterType))
.Select(parameter => new ReplDocOption(
Name: parameter.Name!,
Type: GetFriendlyTypeName(parameter.ParameterType),
Required: IsRequiredParameter(parameter),
Description: parameter.GetCustomAttribute<DescriptionAttribute>()?.Description))
.Select(parameter => BuildDocumentationOption(route.OptionSchema, parameter))
.ToArray();

return new ReplDocCommand(
Expand Down Expand Up @@ -233,6 +230,11 @@ private static string GetFriendlyTypeName(Type type)
return $"{GetFriendlyTypeName(underlying)}?";
}

if (type.IsEnum)
{
return string.Join('|', Enum.GetNames(type));
}

if (!type.IsGenericType)
{
return type.Name.ToLowerInvariant() switch
Expand All @@ -256,4 +258,44 @@ private static string GetFriendlyTypeName(Type type)
var genericArgs = string.Join(", ", type.GetGenericArguments().Select(GetFriendlyTypeName));
return $"{genericName}<{genericArgs}>";
}

private static ReplDocOption BuildDocumentationOption(OptionSchema schema, ParameterInfo parameter)
{
var entries = schema.Entries
.Where(entry => string.Equals(entry.ParameterName, parameter.Name, StringComparison.OrdinalIgnoreCase))
.ToArray();
var aliases = entries
.Where(entry => entry.TokenKind is OptionSchemaTokenKind.NamedOption or OptionSchemaTokenKind.BoolFlag)
.Select(entry => entry.Token)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var reverseAliases = entries
.Where(entry => entry.TokenKind == OptionSchemaTokenKind.ReverseFlag)
.Select(entry => entry.Token)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var valueAliases = entries
.Where(entry => entry.TokenKind is OptionSchemaTokenKind.ValueAlias or OptionSchemaTokenKind.EnumAlias)
.Select(entry => new ReplDocValueAlias(entry.Token, entry.InjectedValue ?? string.Empty))
.GroupBy(alias => alias.Token, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToArray();
var effectiveType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
var enumValues = effectiveType.IsEnum
? Enum.GetNames(effectiveType)
: [];
var defaultValue = parameter.HasDefaultValue && parameter.DefaultValue is not null
? parameter.DefaultValue.ToString()
: null;
return new ReplDocOption(
Name: parameter.Name!,
Type: GetFriendlyTypeName(parameter.ParameterType),
Required: IsRequiredParameter(parameter),
Description: parameter.GetCustomAttribute<DescriptionAttribute>()?.Description,
Aliases: aliases,
ReverseAliases: reverseAliases,
ValueAliases: valueAliases,
EnumValues: enumValues,
DefaultValue: defaultValue);
}
}
2 changes: 1 addition & 1 deletion src/Repl.Core/CoreReplApp.Interactive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ or AmbientCommandOutcome.Handled
}

var invocationTokens = scopeTokens.Concat(inputTokens).ToArray();
var globalOptions = GlobalOptionParser.Parse(invocationTokens, _options.Output);
var globalOptions = GlobalOptionParser.Parse(invocationTokens, _options.Output, _options.Parsing);
var prefixResolution = ResolveUniquePrefixes(globalOptions.RemainingTokens);
if (prefixResolution.IsAmbiguous)
{
Expand Down
Loading