Skip to content

Commit fcf4af5

Browse files
committed
Add examples and how-to guides for custom type converters, dictionary options, and flag arguments in CLI documentation. Reorganize and update related sections.
1 parent ad3aacd commit fcf4af5

16 files changed

Lines changed: 510 additions & 10 deletions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System.ComponentModel;
2+
using System.Globalization;
3+
using Spectre.Console.Cli;
4+
5+
namespace Spectre.Docs.Cli.Examples.DemoApps.CustomTypeConverters;
6+
7+
/// <summary>
8+
/// Demonstrates how to use custom type converters for complex types.
9+
/// </summary>
10+
public class Demo
11+
{
12+
public static async Task<int> RunAsync(string[] args)
13+
{
14+
var app = new CommandApp<DrawCommand>();
15+
return await app.RunAsync(args);
16+
}
17+
}
18+
19+
/// <summary>
20+
/// Represents a 2D point with X and Y coordinates.
21+
/// </summary>
22+
public readonly record struct Point(int X, int Y)
23+
{
24+
public override string ToString() => $"({X}, {Y})";
25+
}
26+
27+
/// <summary>
28+
/// Custom TypeConverter that converts strings like "10,20" to Point.
29+
/// </summary>
30+
public sealed class PointConverter : TypeConverter
31+
{
32+
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
33+
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
34+
35+
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
36+
{
37+
if (value is string str)
38+
{
39+
var parts = str.Split(',');
40+
if (parts.Length == 2 &&
41+
int.TryParse(parts[0].Trim(), out var x) &&
42+
int.TryParse(parts[1].Trim(), out var y))
43+
{
44+
return new Point(x, y);
45+
}
46+
47+
throw new FormatException($"Invalid point format: '{str}'. Expected format: X,Y (e.g., 10,20)");
48+
}
49+
50+
return base.ConvertFrom(context, culture, value);
51+
}
52+
}
53+
54+
/// <summary>
55+
/// A drawing command demonstrating custom type converter usage.
56+
/// </summary>
57+
internal class DrawCommand : Command<DrawCommand.Settings>
58+
{
59+
/// <summary>
60+
/// Settings demonstrating custom type converters.
61+
/// </summary>
62+
public class Settings : CommandSettings
63+
{
64+
// Custom type with TypeConverter attribute
65+
[CommandOption("--point <POINT>")]
66+
[Description("A point in X,Y format (e.g., 10,20)")]
67+
[TypeConverter(typeof(PointConverter))]
68+
public required Point Location { get; init; }
69+
70+
// Optional point with nullable
71+
[CommandOption("--offset [OFFSET]")]
72+
[Description("Optional offset point")]
73+
[TypeConverter(typeof(PointConverter))]
74+
public required FlagValue<Point> Offset { get; init; }
75+
76+
[CommandOption("-c|--color")]
77+
[Description("The drawing color")]
78+
[DefaultValue("black")]
79+
public string Color { get; init; } = "black";
80+
}
81+
82+
protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellation)
83+
{
84+
System.Console.WriteLine($"Drawing at location: {settings.Location}");
85+
System.Console.WriteLine($"Color: {settings.Color}");
86+
87+
if (settings.Offset.IsSet)
88+
{
89+
System.Console.WriteLine($"With offset: {settings.Offset.Value}");
90+
var adjusted = new Point(
91+
settings.Location.X + settings.Offset.Value.X,
92+
settings.Location.Y + settings.Offset.Value.Y);
93+
System.Console.WriteLine($"Adjusted location: {adjusted}");
94+
}
95+
96+
return 0;
97+
}
98+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.ComponentModel;
2+
using Spectre.Console.Cli;
3+
4+
namespace Spectre.Docs.Cli.Examples.DemoApps.DictionaryOptions;
5+
6+
/// <summary>
7+
/// Demonstrates how to use dictionary and lookup options for key-value pairs.
8+
/// </summary>
9+
public class Demo
10+
{
11+
public static async Task<int> RunAsync(string[] args)
12+
{
13+
var app = new CommandApp<ConfigCommand>();
14+
return await app.RunAsync(args);
15+
}
16+
}
17+
18+
/// <summary>
19+
/// A configuration command demonstrating dictionary option patterns.
20+
/// </summary>
21+
internal class ConfigCommand : Command<ConfigCommand.Settings>
22+
{
23+
/// <summary>
24+
/// Settings demonstrating IDictionary, ILookup, and IReadOnlyDictionary options.
25+
/// </summary>
26+
public class Settings : CommandSettings
27+
{
28+
// IDictionary<string, int> - key=value pairs with typed values
29+
// Usage: --value port=8080 --value timeout=30
30+
[CommandOption("--value <VALUE>")]
31+
[Description("Configuration values in key=value format (e.g., port=8080)")]
32+
public IDictionary<string, int>? Values { get; set; }
33+
34+
// ILookup<string, string> - allows multiple values per key
35+
// Usage: --lookup env=dev --lookup env=test --lookup region=us
36+
[CommandOption("--lookup <VALUE>")]
37+
[Description("Lookup values allowing multiple entries per key")]
38+
public ILookup<string, string>? Lookups { get; set; }
39+
40+
// IReadOnlyDictionary<string, string> - immutable key-value pairs
41+
// Usage: --readonly name=myapp --readonly version=1.0
42+
[CommandOption("--readonly <VALUE>")]
43+
[Description("Read-only configuration values")]
44+
public IReadOnlyDictionary<string, string>? ReadOnlyValues { get; set; }
45+
}
46+
47+
protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellation)
48+
{
49+
// Display IDictionary values
50+
if (settings.Values?.Count > 0)
51+
{
52+
System.Console.WriteLine("Values (IDictionary<string, int>):");
53+
foreach (var kvp in settings.Values)
54+
{
55+
System.Console.WriteLine($" {kvp.Key} = {kvp.Value}");
56+
}
57+
}
58+
59+
// Display ILookup values (note: can have multiple values per key)
60+
if (settings.Lookups != null)
61+
{
62+
System.Console.WriteLine("Lookups (ILookup<string, string>):");
63+
foreach (var group in settings.Lookups)
64+
{
65+
var values = string.Join(", ", group);
66+
System.Console.WriteLine($" {group.Key} = [{values}]");
67+
}
68+
}
69+
70+
// Display IReadOnlyDictionary values
71+
if (settings.ReadOnlyValues?.Count > 0)
72+
{
73+
System.Console.WriteLine("ReadOnly Values (IReadOnlyDictionary<string, string>):");
74+
foreach (var kvp in settings.ReadOnlyValues)
75+
{
76+
System.Console.WriteLine($" {kvp.Key} = {kvp.Value}");
77+
}
78+
}
79+
80+
return 0;
81+
}
82+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.ComponentModel;
2+
using Spectre.Console.Cli;
3+
4+
namespace Spectre.Docs.Cli.Examples.DemoApps.FlagArguments;
5+
6+
/// <summary>
7+
/// Demonstrates how to use FlagValue for optional flag arguments.
8+
/// </summary>
9+
public class Demo
10+
{
11+
public static async Task<int> RunAsync(string[] args)
12+
{
13+
var app = new CommandApp<ServerCommand>();
14+
return await app.RunAsync(args);
15+
}
16+
}
17+
18+
/// <summary>
19+
/// A server command demonstrating FlagValue patterns.
20+
/// </summary>
21+
internal class ServerCommand : Command<ServerCommand.Settings>
22+
{
23+
/// <summary>
24+
/// Settings demonstrating FlagValue with optional values.
25+
/// </summary>
26+
public class Settings : CommandSettings
27+
{
28+
// FlagValue<int> - can be used as --port (uses default) or --port 8080 (uses specified)
29+
[CommandOption("--port [PORT]")]
30+
[Description("The port to listen on (default: 3000 if flag present)")]
31+
public required FlagValue<int> Port { get; init; }
32+
33+
// FlagValue<int?> - nullable inner type for truly optional values
34+
[CommandOption("--timeout [SECONDS]")]
35+
[Description("Connection timeout in seconds")]
36+
public required FlagValue<int?> Timeout { get; init; }
37+
38+
// Regular option for comparison
39+
[CommandOption("-h|--host")]
40+
[Description("The host to bind to")]
41+
[DefaultValue("localhost")]
42+
public required string Host { get; init; } = "localhost";
43+
}
44+
45+
protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellation)
46+
{
47+
System.Console.WriteLine($"Host: {settings.Host}");
48+
49+
// Check if --port flag was provided
50+
if (settings.Port?.IsSet == true)
51+
{
52+
// Value is the parsed port number (or default if none specified)
53+
var port = settings.Port.Value;
54+
System.Console.WriteLine($"Port: {port}");
55+
}
56+
else
57+
{
58+
System.Console.WriteLine("Port: not specified (will use system default)");
59+
}
60+
61+
// Check if --timeout flag was provided
62+
if (settings.Timeout?.IsSet == true)
63+
{
64+
if (settings.Timeout.Value.HasValue)
65+
{
66+
System.Console.WriteLine($"Timeout: {settings.Timeout.Value} seconds");
67+
}
68+
else
69+
{
70+
System.Console.WriteLine("Timeout: enabled (no specific value)");
71+
}
72+
}
73+
else
74+
{
75+
System.Console.WriteLine("Timeout: not specified");
76+
}
77+
78+
return 0;
79+
}
80+
}

Spectre.Docs/Content/cli/how-to/async-commands-and-cancellation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Async Commands and Cancellation"
33
description: "How to create asynchronous commands and handle cancellation in Spectre.Console.Cli"
44
uid: "cli-async-commands"
5-
order: 2080
5+
order: 2040
66
---
77

88
When your command performs I/O-bound operations like HTTP requests, database queries, or file operations, use `AsyncCommand<TSettings>` instead of `Command<TSettings>`. This lets you use `async/await` and enables graceful shutdown when users press Ctrl+C.

Spectre.Docs/Content/cli/how-to/configuring-commandapp-and-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Configuring CommandApp and Commands"
33
description: "How to register commands with the CommandApp and configure global settings"
44
uid: "cli-app-configuration"
5-
order: 2090
5+
order: 2050
66
---
77

88
When building a CLI with multiple commands, use `CommandApp.Configure` to register commands, set up aliases, and customize how your application appears in help output.

Spectre.Docs/Content/cli/how-to/customizing-help-text-and-usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Customizing Help Text and Usage"
33
description: "How to tailor the automatically generated help output of Spectre.Console.Cli"
44
uid: "cli-help-customization"
5-
order: 2100
5+
order: 2060
66
---
77

88
Spectre.Console.Cli generates help text automatically from your commands and settings. When the defaults don't match your needs—whether for branding, accessibility, or clarity—you can customize the application name, add usage examples, adjust styling, or disable styling entirely for plain text output.

Spectre.Docs/Content/cli/how-to/defining-commands-and-arguments.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Defining Commands and Arguments"
33
description: "How to declare command-line parameters (arguments and options) using Spectre.Console.Cli's attributes and settings classes"
44
uid: "cli-commands-arguments"
5-
order: 2050
5+
order: 2010
66
---
77

88
Every command in Spectre.Console.Cli receives its input through a `CommandSettings` class. Decorate properties with `[CommandArgument]` for positional parameters and `[CommandOption]` for named flags and options. The framework handles parsing, validation, and help generation automatically.
@@ -56,5 +56,8 @@ Invalid values produce a clear error message listing the allowed options.
5656

5757
## See Also
5858

59+
- <xref:cli-flag-arguments> - Optional flag values with FlagValue
60+
- <xref:cli-dictionary-options> - Key-value pair options with dictionaries
61+
- <xref:cli-custom-type-converters> - Custom type conversion for complex types
5962
- <xref:cli-required-options> - Force users to provide specific options
6063
- <xref:cli-attributes-parameters> - Complete attribute documentation

Spectre.Docs/Content/cli/how-to/handling-errors-and-exit-codes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Handling Errors and Exit Codes"
33
description: "How Spectre.Console.Cli deals with exceptions and how to customize error handling"
44
uid: "cli-error-handling"
5-
order: 2070
5+
order: 2030
66
---
77

88
By default, Spectre.Console.Cli catches exceptions, displays a user-friendly message, and returns exit code `-1`. When you need more control—different exit codes for different error types, custom formatting, or integration with logging—you have two options: `SetExceptionHandler` for centralized handling within the framework, or `PropagateExceptions` for full manual control with try-catch blocks.

Spectre.Docs/Content/cli/how-to/hiding-commands-and-options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Hiding Commands and Options"
33
description: "How to hide commands and options from help output while keeping them functional"
44
uid: "cli-hidden-commands"
5-
order: 2130
5+
order: 2120
66
---
77

88
Sometimes you need commands or options that work but shouldn't appear in help output—internal debugging tools, deprecated features you're phasing out, or advanced options that would overwhelm typical users. Hidden items remain fully functional; users who know about them can still use them.

Spectre.Docs/Content/cli/how-to/intercepting-command-execution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Intercepting Command Execution"
33
description: "How to use command interceptors to run logic before or after any command executes"
44
uid: "cli-command-interception"
5-
order: 2140
5+
order: 2130
66
---
77

88
When you need cross-cutting concerns like logging, timing, or authentication across all commands without duplicating code, implement a command interceptor. The interceptor runs before and after every command, keeping individual commands focused on their core logic.

0 commit comments

Comments
 (0)