Skip to content

Commit 88bf0e1

Browse files
committed
Overhaul documentation: split into focused guides with comprehensive coverage
Rewrite root readme.md with concise features list, quick start, and documentation table. Rewrite doc/readme.md as an index page linking to individual guides. New documentation pages: - doc/getting-started.md: installation, first app, options, sub-commands, arguments - doc/options.md: prototype syntax, flags, values, key/value, typed, collections, env vars - doc/commands.md: CommandApp, sub-commands, groups, actions, parsing flow - doc/arguments.md: positional arguments, cardinality, typed, validation - doc/validation.md: built-in validators, custom validators, constraints - doc/help-output.md: help text, CommandUsage, ICommandOutput, Terminal package - doc/advanced.md: parse API, shell completions, response files, config, performance
1 parent 30d1946 commit 88bf0e1

9 files changed

Lines changed: 1955 additions & 931 deletions

File tree

doc/advanced.md

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# Advanced Topics
2+
3+
This guide covers advanced features of XenoAtom.CommandLine: the parse API for testing, shell completions, response files, configuration, and performance.
4+
5+
## Parse API
6+
7+
`CommandApp.Parse(...)` runs the full parsing pipeline — option callbacks, argument binding, constraint checks — but **does not** invoke the command action. This is ideal for unit testing:
8+
9+
```csharp
10+
string? name = null;
11+
int port = 0;
12+
13+
var app = new CommandApp("myexe")
14+
{
15+
{ "n|name=", "Your {NAME}", v => name = v },
16+
{ "p|port=", "Server {PORT}", (int v) => port = v },
17+
new HelpOption(),
18+
(ctx, _) => ValueTask.FromResult(0)
19+
};
20+
21+
var result = app.Parse(["--name", "Alice", "--port", "8080"]);
22+
23+
// result.HasErrors → false
24+
// result.ResolvedCommandPath → "myexe"
25+
// result.OptionValues["name"][0] → "Alice"
26+
// result.OptionValues["port"][0] → "8080"
27+
// name → "Alice" (option callbacks are invoked)
28+
// port → 8080
29+
```
30+
31+
### ParseResult Properties
32+
33+
| Property | Type | Description |
34+
|---|---|---|
35+
| `ResolvedCommand` | `Command` | The command that was resolved |
36+
| `ResolvedCommandPath` | `string` | Full command path (e.g. `"myexe commit"`) |
37+
| `OptionValues` | `IReadOnlyDictionary<string, IReadOnlyList<string?>>` | Parsed option values by name |
38+
| `ArgumentValues` | `IReadOnlyList<string>` | Parsed positional argument values |
39+
| `RemainingArguments` | `IReadOnlyList<string>` | Remaining unbound arguments |
40+
| `Errors` | `IReadOnlyList<CommandException>` | Parse errors (empty on success) |
41+
| `HasErrors` | `bool` | Whether any errors occurred |
42+
| `HelpRequested` | `bool` | Whether `--help` was passed |
43+
| `VersionRequested` | `bool` | Whether `--version` was passed |
44+
45+
### Parse Behavior
46+
47+
- Option and argument action callbacks **are** invoked during parsing.
48+
- The command action is **not** invoked.
49+
- Help/version requests are surfaced via `HelpRequested`/`VersionRequested`.
50+
- Errors are collected in `Errors` instead of being written to stderr.
51+
- If `runConfig` is null, `Out` and `Error` default to `TextWriter.Null` to suppress output.
52+
53+
### Testing Sub-Commands
54+
55+
```csharp
56+
var result = app.Parse(["commit", "--message", "Hello"]);
57+
58+
// result.ResolvedCommandPath → "myexe commit"
59+
// result.OptionValues["message"][0] → "Hello"
60+
```
61+
62+
## Shell Completions
63+
64+
XenoAtom.CommandLine can generate shell completion scripts and provide completion candidates for partially typed command lines.
65+
66+
### Adding Completion Support
67+
68+
Add `CompletionCommands` to your app to expose completion commands:
69+
70+
```csharp
71+
var app = new CommandApp("myexe")
72+
{
73+
new CompletionCommands(),
74+
{ "n|name=", "Your {NAME}", v => {} },
75+
new HelpOption(),
76+
new Command("build", "Build the project") { (ctx, _) => ValueTask.FromResult(0) },
77+
(ctx, _) => ValueTask.FromResult(0)
78+
};
79+
```
80+
81+
This adds two sub-commands:
82+
- `completion <shell>` — generates the shell completion script
83+
- `__complete` (hidden) — handles completion requests from the shell
84+
85+
### Installing Completions
86+
87+
Generate and source the completion script for your shell:
88+
89+
```sh
90+
# Bash (current session)
91+
eval "$(myexe completion bash)"
92+
93+
# Zsh (current session)
94+
source <(myexe completion zsh)
95+
96+
# Fish (current session)
97+
myexe completion fish | source
98+
99+
# PowerShell (current session)
100+
myexe completion powershell | Out-String | Invoke-Expression
101+
```
102+
103+
### Programmatic Completions
104+
105+
You can also get completion candidates programmatically:
106+
107+
```csharp
108+
var candidates = app.GetCompletions("myexe --na");
109+
// → ["--name"]
110+
111+
var candidates = app.GetCompletionsForTokens(["myexe", "buil"], tokenIndex: 1);
112+
// → ["build"]
113+
```
114+
115+
### Value Completions
116+
117+
Provide completion candidates for option and argument values:
118+
119+
```csharp
120+
app.Options["name"].ValueCompleter = static (index, prefix) =>
121+
["Alice", "Bob", "Charlie"];
122+
123+
app.Arguments[0].ValueCompleter = static (index, prefix) =>
124+
["README.md", "src/", "tests/"];
125+
```
126+
127+
The `ValueCompleter` delegate receives:
128+
- `index` — the 0-based value index being completed
129+
- `prefix` — the partially typed text
130+
131+
### Completion Protocol
132+
133+
The shell scripts call the hidden `__complete` sub-command using one of two modes:
134+
135+
- **Token mode** (preferred): `myexe __complete --command-name <NAME> --index <N> --token <T1> --token <T2> ...`
136+
- **Line mode** (fallback): `myexe __complete --command-name <NAME> --line <LINE> --cursor <POS>`
137+
138+
Completion is **non-executing** — it does not invoke user option actions. It only inspects the declared command tree.
139+
140+
## Response Files
141+
142+
Response files let users put arguments in a file and reference it with `@`:
143+
144+
### Enabling Response Files
145+
146+
Add `ResponseFileSource` to your command:
147+
148+
```csharp
149+
var app = new CommandApp("myexe")
150+
{
151+
new HelpOption(),
152+
new ResponseFileSource(),
153+
{ "<>", "Extra arguments" },
154+
(ctx, arguments) =>
155+
{
156+
foreach (var arg in arguments)
157+
ctx.Out.WriteLine(arg);
158+
return ValueTask.FromResult(0);
159+
}
160+
};
161+
```
162+
163+
Help output includes:
164+
165+
```
166+
@file Read response file for more options.
167+
```
168+
169+
### Using Response Files
170+
171+
Create a response file (e.g. `args.txt`):
172+
173+
```
174+
--name John
175+
--port 8080
176+
# This is a comment
177+
"hello world"
178+
```
179+
180+
Pass it with `@`:
181+
182+
```sh
183+
myexe @args.txt
184+
```
185+
186+
### Response File Syntax
187+
188+
| Feature | Syntax | Example |
189+
|---|---|---|
190+
| Whitespace separation | spaces/tabs | `--name John``--name`, `John` |
191+
| Quoted values | `"..."` or `'...'` | `"hello world"``hello world` |
192+
| Comments | `#` at start of line | `# This is a comment` |
193+
| Escaping (non-Windows) | `\` | `c\ d``c d` |
194+
| Literal backslash (Windows) | `\` is literal | `C:\Temp\file.txt``C:\Temp\file.txt` |
195+
196+
### Custom Argument Sources
197+
198+
You can create your own argument source by extending `ArgumentSource`:
199+
200+
```csharp
201+
public class EnvironmentSource : ArgumentSource
202+
{
203+
public override string Description => "Read arguments from environment";
204+
public override string[] GetNames() => ["@env"];
205+
public override bool TryGetArguments(string value, out IEnumerable<string>? arguments)
206+
{
207+
if (value.StartsWith("@env:"))
208+
{
209+
var envValue = Environment.GetEnvironmentVariable(value[5..]);
210+
arguments = envValue?.Split(' ') ?? [];
211+
return true;
212+
}
213+
arguments = null;
214+
return false;
215+
}
216+
}
217+
```
218+
219+
## Configuration
220+
221+
### CommandConfig
222+
223+
`CommandConfig` controls application-level behavior. It is set once when creating the `CommandApp`:
224+
225+
```csharp
226+
var config = new CommandConfig
227+
{
228+
StrictOptionParsing = true, // default
229+
Localizer = s => s, // identity by default
230+
EnvironmentVariableResolver = Environment.GetEnvironmentVariable, // default
231+
OutputFactory = runConfig => new MyOutputRenderer(),
232+
};
233+
234+
var app = new CommandApp("myexe", config: config);
235+
```
236+
237+
| Property | Default | Description |
238+
|---|---|---|
239+
| `StrictOptionParsing` | `true` | Fail on unknown `-`/`--` tokens |
240+
| `Localizer` | Identity | Transform all built-in strings (for localization) |
241+
| `EnvironmentVariableResolver` | `Environment.GetEnvironmentVariable` | Customize env-var lookup |
242+
| `OutputFactory` | `null` (uses `DefaultCommandOutput`) | Factory for custom output rendering |
243+
244+
### CommandRunConfig
245+
246+
`CommandRunConfig` controls runtime behavior for a specific invocation:
247+
248+
```csharp
249+
var runConfig = new CommandRunConfig(Width: 120, OptionWidth: 32)
250+
{
251+
Out = Console.Out,
252+
Error = Console.Error,
253+
ShowLicenseOnRun = true,
254+
};
255+
256+
await app.RunAsync(args, runConfig);
257+
```
258+
259+
| Property | Default | Description |
260+
|---|---|---|
261+
| `Width` | `80` | Terminal width for formatting |
262+
| `OptionWidth` | `29` | Column width for option names in help |
263+
| `Out` | `Console.Out` | Standard output writer |
264+
| `Error` | `Console.Error` | Standard error writer |
265+
| `ShowLicenseOnRun` | `true` | Whether to print the license header |
266+
267+
### Localization
268+
269+
Use `CommandConfig.Localizer` to translate all built-in strings:
270+
271+
```csharp
272+
var app = new CommandApp("myexe", config: new CommandConfig
273+
{
274+
Localizer = text => MyLocalizationService.Translate(text),
275+
});
276+
```
277+
278+
The localizer is applied before strings are written to `Out`/`Error`.
279+
280+
### Environment Variable Resolution
281+
282+
Override environment variable lookup for testing or custom scenarios:
283+
284+
```csharp
285+
var envVars = new Dictionary<string, string> { ["MY_PORT"] = "8080" };
286+
287+
var app = new CommandApp("myexe", config: new CommandConfig
288+
{
289+
EnvironmentVariableResolver = name => envVars.GetValueOrDefault(name),
290+
});
291+
```
292+
293+
## EnumWrapper
294+
295+
`EnumWrapper<T>` provides AOT-friendly enum parsing for options:
296+
297+
```csharp
298+
var colors = new List<Color>();
299+
300+
var app = new CommandApp("myexe")
301+
{
302+
{ "c|color=", $"Console {{COLOR}} ({EnumWrapper<Color>.Names})",
303+
(EnumWrapper<Color> v) => colors.Add(v) },
304+
(ctx, _) => ValueTask.FromResult(0)
305+
};
306+
307+
enum Color { Red, Green, Blue }
308+
```
309+
310+
`EnumWrapper<T>`:
311+
- Implements `ISpanParsable<T>` for seamless integration with the option parser.
312+
- Provides case-insensitive parsing.
313+
- Has a static `Names` property returning a comma-separated list of valid values.
314+
- Supports implicit conversion to and from the wrapped enum type.
315+
316+
## Performance
317+
318+
The parser is optimized for minimal allocations:
319+
320+
- No regex usage during parsing
321+
- No per-option `string.Split` arrays
322+
- Optimized hot paths for option lookup and value extraction
323+
324+
### Benchmarking
325+
326+
Run the included benchmarks:
327+
328+
```sh
329+
dotnet run -c Release --project src/XenoAtom.CommandLine.Benchmarks
330+
```
331+
332+
### NativeAOT Compatibility
333+
334+
XenoAtom.CommandLine is fully compatible with NativeAOT publishing:
335+
336+
- No reflection usage
337+
- All types are trimmer-safe
338+
- `EnumWrapper<T>` avoids runtime reflection for enum parsing
339+
340+
Publish with NativeAOT:
341+
342+
```sh
343+
dotnet publish -c Release -r win-x64 /p:PublishAot=true
344+
```
345+
346+
## Class Diagram
347+
348+
The following diagram shows the main types and their relationships:
349+
350+
![Class diagram](XenoAtom.CommandLine.png)
351+
352+
The design is intentionally simple. The main types are:
353+
354+
- **`CommandApp`** — entry point, inherits from `Command`
355+
- **`Command`** — executable command with options, arguments, and sub-commands
356+
- **`CommandGroup`** — conditional group of nodes
357+
- **`Option`** — option with prototype, description, and callback
358+
- **`CommandArgument`** — positional argument with cardinality
359+
- **`HelpOption`** / **`VersionOption`** — built-in options
360+
- **`ResponseFileSource`**`@file` argument expansion
361+
- **`CompletionCommands`** — shell completion support
362+
- **`CommandUsage`** — usage text for help
363+
364+
All these types inherit from `CommandNode`, the base class for all command tree nodes. Container types (`Command`, `CommandApp`, `CommandGroup`) inherit from `CommandContainer` and support collection initializers.
365+
366+
## Next Steps
367+
368+
- [Getting Started](getting-started.md) — first application walkthrough
369+
- [Options](options.md) — full option reference
370+
- [Commands](commands.md) — sub-commands and groups
371+
- [Arguments](arguments.md) — positional arguments
372+
- [Validation & Constraints](validation.md) — validate values and constraints
373+
- [Help & Output](help-output.md) — customize help rendering

0 commit comments

Comments
 (0)