diff --git a/.openpublishing.redirection.fundamentals.json b/.openpublishing.redirection.fundamentals.json index 55f65d3db9307..9f2ab85ef25de 100644 --- a/.openpublishing.redirection.fundamentals.json +++ b/.openpublishing.redirection.fundamentals.json @@ -168,6 +168,30 @@ "source_path_from_root": "/docs/fundamentals/networking/httpclient.md", "redirect_url": "/dotnet/fundamentals/networking/http/httpclient" }, + { + "source_path_from_root": "/docs/standard/commandline/customize-help.md", + "redirect_url": "/docs/standard/commandline/help" + }, + { + "source_path_from_root": "/docs/standard/commandline/define-commands.md", + "redirect_url": "/docs/standard/commandline/syntax#commands" + }, + { + "source_path_from_root": "/docs/standard/commandline/dependency-injection.md", + "redirect_url": "/docs/standard/commandline/beta5#invocation" + }, + { + "source_path_from_root": "/docs/standard/commandline/handle-termination.md", + "redirect_url": "/docs/standard/commandline/parse-and-invoke#process-termination-timeout" + }, + { + "source_path_from_root": "/docs/standard/commandline/model-binding.md", + "redirect_url": "/docs/standard/commandline/parse-and-invoke" + }, + { + "source_path_from_root": "/docs/standard/commandline/use-middleware.md", + "redirect_url": "/docs/standard/commandline/beta5#configuration" + }, { "source_path_from_root": "/docs/fundamentals/networking/httpclient-guidelines.md", "redirect_url": "/dotnet/fundamentals/networking/http/httpclient-guidelines" diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index cbd051f5dba7d..f5c446be266e4 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -934,22 +934,22 @@ items: href: ../standard/commandline/index.md - name: Get started tutorial href: ../standard/commandline/get-started-tutorial.md - - name: Command-line syntax + - name: Command-line syntax overview href: ../standard/commandline/syntax.md - - name: Define commands - href: ../standard/commandline/define-commands.md - - name: Model binding - href: ../standard/commandline/model-binding.md - - name: Tab completion + - name: How to parse and invoke the result + href: ../standard/commandline/parse-and-invoke.md + - name: How to customize parsing and validation + href: ../standard/commandline/parsing-and-validation.md + - name: How to configure the parser + href: ../standard/commandline/command-line-configuration.md + - name: How to enable and customize tab completion href: ../standard/commandline/tab-completion.md - - name: Dependency injection - href: ../standard/commandline/dependency-injection.md - - name: Customize help - href: ../standard/commandline/customize-help.md - - name: Handle termination - href: ../standard/commandline/handle-termination.md - - name: Use middleware - href: ../standard/commandline/use-middleware.md + - name: How to customize help + href: ../standard/commandline/help.md + - name: Design guidance + href: ../standard/commandline/design-guidance.md + - name: Breaking changes in beta5 + href: ../standard/commandline/beta5.md - name: File and stream I/O items: - name: Overview diff --git a/docs/standard/commandline/beta5.md b/docs/standard/commandline/beta5.md new file mode 100644 index 0000000000000..9d32dde829d8e --- /dev/null +++ b/docs/standard/commandline/beta5.md @@ -0,0 +1,330 @@ +--- +title: System.CommandLine beta5 breaking changes +description: "Learn what breaking changes were introduced in beta5 and why." +ms.date: 16/06/2025 +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" +ms.topic: how-to +--- + +# Breaking changes in beta5 + +[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] + +Our main focus for beta5 was to improve the APIs and take a step toward releasing a stable version of System.CommandLine. We have simplified the APIs and made them more consistent and coherent with the [Framework design guidelines](../design-guidelines/index.md). This article describes the breaking changes that were made in beta5 and the reasoning behind them. + +## Renaming + +In beta4, not all types and properties followed the [naming guidelines](../design-guidelines/naming-guidelines.md). Some were not consistent with the naming conventions, such as using the `Is` prefix for boolean properties. In beta5, we have renamed some types and properties. The following table shows the old and new names: + +| Old name | New name | +|-------------------------------------------------------------|----------------------------------------------------------------| +| `System.CommandLine.Parsing.Parser` | `System.CommandLine.Parsing.CommandLineParser` | +| `System.CommandLine.Parsing.OptionResult.IsImplicit` | `System.CommandLine.Parsing.OptionResult.Implicit` | +| `System.CommandLine.Option.IsRequired` | `System.CommandLine.Option.Required` | +| `System.CommandLine.Symbol.IsHidden` | `System.CommandLine.Symbol.Hidden` | +| `System.CommandLine.Option.ArgumentHelpName` | `System.CommandLine.Option.HelpName` | +| `System.CommandLine.Parsing.OptionResult.Token` | `System.CommandLine.Parsing.OptionResult.IdentifierToken` | +| `System.CommandLine.Parsing.ParseResult.FindResultFor` | `System.CommandLine.Parsing.ParseResult.GetResult` | +| `System.CommandLine.Parsing.SymbolResult.ErrorMessage` | `System.CommandLine.Parsing.SymbolResult.AddError` | + +In case of the `ErrorMessage` property, we changed the name to `AddError` and made it a method. The goal was to allow to report multiple errors for the same symbol, which so far was impossible. + +## Exposing mutable collections + +In beta4, we had many `Add` methods that were used to add items to collections, such as arguments, options, subcommands, validators, and completions. Some of these collections were exposed via properties as read-only collections. Because of that, it was impossible to remove items from those collections. + +In beta5, we changed the APIs to expose mutable collections instead of `Add` methods and (sometimes) read-only collections. This allows you to not only add items or enumerate them, but also remove them. The following table shows the old method and new property names: + +| Old method name | New property | +|-------------------------------------------------------------|----------------------------------------------------------------| +| `System.CommandLine.Command.AddArgument` | `System.CommandLine.Command.Arguments` | +| `System.CommandLine.Command.AddOption` | `System.CommandLine.Command.Options` | +| `System.CommandLine.Command.AddCommand` | `System.CommandLine.Command.Subcommands` | +| `System.CommandLine.Command.AddValidator` | `System.CommandLine.Command.Validators` | +| `System.CommandLine.Option.AddValidator` | `System.CommandLine.Option.Validators` | +| `System.CommandLine.Argument.AddValidator` | `System.CommandLine.Argument.Validators` | +| `System.CommandLine.Command.AddCompletions` | `System.CommandLine.Command.CompletionSources` | +| `System.CommandLine.Option.AddCompletions` | `System.CommandLine.Option.CompletionSources` | +| `System.CommandLine.Argument.AddCompletions` | `System.CommandLine.Argument.CompletionSources` | +| `System.CommandLine.Command.AddAlias` | `System.CommandLine.Command.Aliases` | +| `System.CommandLine.Option.AddAlias` | `System.CommandLine.Option.Aliases` | + +The `RemoveAlias` and `HasAlias` methods were also removed, as the `Aliases` property is now a mutable collection. You can use the `Remove` method to remove an alias from the collection, and `Contains` method to check if an alias exists. + +## Names and aliases + +Before beta5, there was no clear separation between the name and [aliases](syntax.md#aliases) of a symbol. When `name` was not provided for the `Option` constructor, the symbol reported its name as the longest alias with prefixes like `--`, `-`, or `/` removed. That was confusing. + +Moreover, to get the parsed value, users had to store a reference to an option or an argument and then use it to get the value from `ParseResult`. + +To promote simplicity and explicitness, we decided to make the name of a symbol a mandatory parameter for every symbol constructor (including `Argument`). We have also separated the concept of a name and aliases; now aliases are just aliases and do not include the name of the symbol. Of course, they are optional. As a result, we made the following changes: + +- `name` is now a mandatory argument for every public constructor of `Argument`, `Option`, and `Command`. In the case of `Argument`, it is not used for parsing, but to generate the help and completions text. In the case of `Option` and `Command`, it is used to identify the symbol during parsing and also for help and completions. +- The `Symbol.Name` property is no longer `virtual`; it's now read-only and returns the name as it was provided when the symbol was created. Because of that, `Symbol.DefaultName` was removed and `Option.Name` no longer removes the `--`, `-`, or `/` or any other prefix from the longest alias. +- The `Aliases` property exposed by `Option` and `Command` is now a mutable collection. This collection no longer includes the name of the symbol. +- `System.CommandLine.Parsing.IdentifierSymbol` was removed (it was a base type for both `Command` and `Option`). + +Having the name always present allows for [getting the parsed value by name](parse-and-invoke.md#getvalue): + +```csharp +RootCommand command = new("The description.") +{ + new Option("--number") +}; + +ParseResult parseResult = command.Parse(args); +int number = parseResult.GetValue("--number"); +``` + +### Creating options with aliases + +In the past, `Option` was exposing plenty of constructors, some of which were accepting the name or not. Since the name is now mandatory and we expect aliases to be frequetly provided for `Option`, we provide only a single constrtor. It accepts the name and a `params` array of aliases. + +Before beta5, `Option` had a constructor that took a name and a description. Because of that, the second argument might now be treated as an alias rather than a description. It's the only known breaking change in the API that is not going to cause a compiler error. + +Old code that used the constructor with a description should be updated to use the new constructor that takes a name and aliases, and then set the `Description` property separately. For example: + +```csharp +Option beta4 = new("--help", "An option with aliases."); +beta4b.Aliases.Add("-h"); +beta4b.Aliases.Add("/h"); + +Option beta5 = new("--help", "-h", "/h") +{ + Description = "An option with aliases." +}; +``` + +## Default values + +In beta4, users could set default values for options and arguments by using the `SetDefaultValue` methods. Those methods were accepting an `object` value, which was not type-safe and could lead to runtime errors if the value was not compatible with the option or argument type: + +```csharp +Option option = new("--number"); +option.SetDefaultValue("text"); // This is not type-safe, as the value is a string, not an int. +``` + +Moreover, some of the `Option` and `Argument` constructors accepted a parse delegate and a boolean indicating whether the delegate was a custom parser or a default value provider. This was confusing. + +`Option` and `Argument` classes now have a `DefaultValueFactory` property that can be used to set the default value for the symbol. This property is invoked when the symbol is not provided in the command line input. + +```csharp +Option number = new("--number") +{ + DefaultValueFactory = _ => 42 +}; +``` + +`Argument` and `Option` also come with a `CustomParser` property that can be used to set a custom parser for the symbol: + +```csharp +Argument uri = new("arg") +{ + CustomParser = result => + { + if (!Uri.TryCreate(result.Tokens.Single().Value, UriKind.RelativeOrAbsolute, out var uriValue)) + { + result.AddError("Invalid URI format."); + return null; + } + + return uriValue; + } +}; +``` + +Moreover, `CustomParser` accepts `Func` delegate, rather than dedicated `ParseArgument` delegate. This and few other custom delegates were removed to simplify the API and reduce the number of types exposed by the API and compiled at startup time by the JIT compiler. + +For more examples of how to use `DefaultValueFactory` and `CustomParser`, see the [How to customize parsing and validation in System.CommandLine](parsing-and-validation.md) document. + +## The separation of parsing and invoking + +In beta4, it was possible to separate the parsing and invoking of commands, but it was quite unclear how to do it. The `Command` was not exposing a `Parse` method, but `CommandExtensions` was providing `Parse`, `Invoke`, and `InvokeAsync` extension methods for `Command`. This was confusing, as it was not clear which method to use and when. Following changes were made to simplify the API: + +- `Command` now exposes a `Parse` method that returns a `ParseResult` object. This method is used to parse the command line input and return the result of the parsing. Moreover, it makes it clear that the command is not invoked, but only parsed and only in synchronous manner. +- `ParseResult` now exposes both `Invoke` and `InvokeAsync` methods that can be used to invoke the command. This makes it clear that the command is invoked after parsing, and allows for both synchronous and asynchronous invocation. +- `CommandExtensions` class was removed, as it was not needed anymore. + +### Configuration + +Before beta5, it was possible to customize the parsing, but only with some of the public `Parse` methods. There was a `Parser` class that was exposing two public constructors: one accepting a `Command` and another accepting a `CommandLineConfiguration`. `CommandLineConfiguration` was immutable and in order to create it, the users had to use a builder pattern exposed by the `CommandLineBuilder` class. Following changes were made to simplify the API: + +- `CommandLineConfiguration` was made mutable and `CommandLineBuilder` was removed. Creating a configuration is now as simple as creating an instance of `CommandLineConfiguration` and setting the properties you want to customize. Moreover, creating a new instance of configuration is the equivalent of calling `CommandLineBuilder`'s `UseDefaults` method. +- Every `Parse` method now accepts an optional `CommandLineConfiguration` parameter that can be used to customize the parsing. When it's not provided, the default configuration is used. +- `Parser` was renamed to `CommandLineParser` to disambiguate from other parser types to avoid name conflicts. Since it's stateless, it's now a static class with only static methods. It exposes two `Parse` parse methods: one accepting a `IReadOnlyList args` and another accepting a `string args`. The latter uses `CommandLineParser.SplitCommandLine` (also public) to split the command line input into [tokens](syntax.md#tokens) before parsing it. + +`CommandLineBuilderExtensions` was also removed, here is how you can map the its methods to the new APIs: + +- `CancelOnProcessTermination` is now a property of `CommandLineConfiguration` called [ProcessTerminationTimeout](parse-and-invoke.md#process-termination-timeout). It's enabled by default, with a 2s timeout. Set it to `null` to disable it. +- `EnableDirectives`, `UseEnvironmentVariableDirective`, `UseParseDirective` and `UseSuggestDirective` were removed. A new [Directive](syntax.md#directives) type was introduced and the [RootCommand](syntax.md#root-command) now exposes property. You can add, remove and iterate directives by using this collection. [Suggest directive](syntax.md#suggest-directive) is included by default, you can also use other directives like [DiagramDirective](syntax.md#the-diagram-directive) or `EnvironmentVariablesDirective`. +- `EnableLegacyDoubleDashBehavior` was removed. All unmatched tokens are now exposed by the [ParseResult.UnmatchedTokens](parse-and-invoke.md#unmatched-tokens) property. +- `EnablePosixBundling` was removed. The bundling is now enabled by default, you can disable it by setting the [CommandLineConfiguration.EnableBundling](command-line-configuration.md#enableposixbundling) property to `false`. +- `RegisterWithDotnetSuggest` was removed as it was performing very expensive operation, typically during application startup. The users are now required to register their commands with `dotnet suggest` [manually](tab-completion.md#enable-tab-completion). +- `UseExceptionHandler` was removed. The default exception handler is now enabled by default, you can disable it by setting the [CommandLineConfiguration.EnableDefaultExceptionHandler](command-line-configuration.md#enabledefaultexceptionhandler) property to `false`. This is useful when you want to handle exceptions in a custom way, by just wrapping the `Invoke` or `InvokeAsync` methods in a try-catch block. +- `UseHelp` and `UseVersion` were removed. The help and version are now exposed by the [HelpOption](help.md#help-option) and [VersionOption](syntax.md#version-option) public types. They are both included by default in the options defined by [RootCommand](syntax.md#root-command). +- `UseHelpBuilder` was removed. Please read the [How to customize help in System.CommandLine](help.md) document for more information on how to customize the help output. +- `AddMiddleware` was removed. It was slowing down the application startup a lot, and since we were able to express all the features without it, we decided to remove it. +- `UseParseErrorReporting` and `UseTypoCorrections` were removed. The parse errors are now reported by default when invoking `ParseResult`. You can configure it by using the `ParseErrorAction` exposed by `ParseResult.Action` property. + +```csharp +ParseResult result = rootCommand.Parse("myArgs", config); +if (result.Action is ParseErrorAction parseError) +{ + parseError.ShowTypoCorrections = true; + parseError.ShowHelp = false; +} +``` + +- `UseLocalizationResources` and `LocalizationResources` were removed. This feature was used mostly by the `dotnet` CLI to add missing translations to `System.CommandLine`. All those translations were moved to the System.CommandLine itself, so this feature is no longer needed. If we are missing support for your language, please [report an issue](https://github.com/dotnet/command-line-api/issues/new/choose). +- `UseTokenReplacer` was removed. [Response files](syntax.md#response-files) are enabled by default, but you can disable them by setting the property to `null`. You can also provide a custom implementation to customize how response files are processed. + +Last but not least, the `IConsole` and all related interfaces (`IStandardOut`, `IStandardError`, `IStandardIn`) were removed. exposes two `TextWriter` properties: `Output` and `Error`. These can be set to any `TextWriter` instance, such as a `StringWriter`, which can be used to capture output for testing. Our motivation was to expose fewer types and reuse existing abstractions. + +### Invocation + +In beta4, the `ICommandHandler` interface was exposing `Invoke` and `InvokeAsync` methods that were used to invoke the parsed command. This was making it easy to mix synchronous and asynchronous code, for example by defining a synchronous handler for a command and then invoking it asynchronously (which could lead to a [deadlock](../../csharp/asynchronous-programming/index.md#dont-block-await-instead)). Moreover, it was possible to define a handler only for a command. Not for an option (like help, which displays help) or a directive. + +We decided to introduce a new abstract base class and two derived classes: and . The former is used for synchronous actions that return an `int` exit code, while the latter is used for asynchronous actions that return a `Task` exit code. + +You don't need to create a derived type to define an action. You can use the method to set an action for a command. The synchronous action can be a delegate that takes a parameter and returns an `int` exit code (or nothing, and then a default `0` exit code is returned). The asynchronous action can be a delegate that takes a and parameters and returns a `Task` (or `Task` to get default exit code returned). + +```csharp +rootCommand.SetAction(ParseResult parseResult => +{ + FileInfo parsedFile = parseResult.GetValue(fileOption); + ReadFile(parsedFile); +}); +``` + +In the past, the `CancellationToken` passed to `InvokeAsync` was exposed to handler via a method of `InvocationContext`: + +```csharp +rootCommand.SetHandler(async (InvocationCotnext context) => +{ + string? urlOptionValue = context.ParseResult.GetValueForOption(urlOption); + var token = context.GetCancellationToken(); + returnCode = await DoRootCommand(urlOptionValue, token); +}); +``` + +Majority of our users were not obtaining this token and passing it further. We made `CancellationToken` mandatory argument for asynchronous actions, in order for the compiler to produce a warning when it's not passed further ([CA2016](../../fundamentals/code-analysis/quality-rules/ca2016.md)). + +```csharp +rootCommand.SetAction((ParseResult parseResult, CancellationToken token) => +{ + string? urlOptionValue = parseResult.GetValue(urlOption); + return DoRootCommandAsync(urlOptionValue, token); +}); +``` + +As a result of these and other forementioned changes, the `InvocationContext` class got also removed. The `ParseResult` is now passed directly to the action, so you can access the parsed values and options directly from it. + +To summarize these changes: + +- The `ICommandHandler` interface was removed. `SynchronousCommandLineAction` and `AsynchronousCommandLineAction` were introduced. +- The `Command.SetHandler` method was renamed to `SetAction`. +- The `Command.Handler` property was renamed to `Command.Action`. Option got extended with `Option.Action`. +- `InvocationContext` was removed. The `ParseResult` is now passed directly to the action. + +For more details about how to use actions, see the [How to parse and invoke commands in System.CommandLine](parse-and-invoke.md) document. + +## The benefits of the simplified API + +We hope that the changes made in beta5 will make the API more consistent, futureproof and easier to use for existing and new users. + +The new users will need to learn fewer concepts and types, as the number of public interfaces dropped from 11 to 0, public classes (and structs) from 56 to 38. Public method count dropped from 378 to 235, and public properties from 118 to 99. + +We have also reduced the number of assemblies referenced by System.CommandLine from 11 to 6: + +```diff +System.Collections +- System.Collections.Concurrent +- System.ComponentModel +System.Console +- System.Diagnostics.Process +System.Linq +System.Memory +- System.Net.Primitives +System.Runtime +- System.Runtime.Serialization.Formatters ++ System.Runtime.InteropServices +- System.Threading +``` + +It allowed us to reduce the size of the library by 32% and the size of the following NativeAOT app by 20% (on Windows): + +```csharp +Option boolOption = new Option(new[] { "--bool", "-b" }, "Bool option"); +Option stringOption = new Option(new[] { "--string", "-s" }, "String option"); + +RootCommand command = new RootCommand +{ + boolOption, + stringOption +}; + +command.SetHandler(Run, boolOption, stringOption); + +return new CommandLineBuilder(command).UseDefaults().Build().Invoke(args); + +static void Run(bool boolean, string text) +{ + Console.WriteLine($"Bool option: {text}"); + Console.WriteLine($"String option: {boolean}"); +} +``` + +```csharp +Option boolOption = new Option("--bool", "-b") { Description = "Bool option" }; +Option stringOption = new Option("--string", "-s") { Description = "String option" }; + +RootCommand command = new () +{ + boolOption, + stringOption, +}; + +command.SetAction(parseResult => Run(parseResult.GetValue(boolOption), parseResult.GetValue(stringOption))); + +return new CommandLineConfiguration(command).Invoke(args); + +static void Run(bool boolean, string text) +{ + Console.WriteLine($"Bool option: {text}"); + Console.WriteLine($"String option: {boolean}"); +} +``` + +Simplicity has also improved the performance of the library (it's a side effect of our work, not the main goal of it). The [benchmarks](https://github.com/adamsitnik/commandline-perf/tree/update) show that the parsing and invoking of commands is now faster than in beta4, especially for large commands with many options and arguments. The performance improvements are visible in both synchronous and asynchronous scenarios. + +For the simplest app presented above, we got the following results: + +```ini +BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4061/24H2/2024Update/HudsonValley) +AMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores +.NET SDK 9.0.300 + [Host] : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2 + Job-JJVAFK : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2 + +EvaluateOverhead=False OutlierMode=DontRemove InvocationCount=1 +IterationCount=100 UnrollFactor=1 WarmupCount=3 + +| Method | Args | Mean | StdDev | Ratio | +|------------------------ |--------------- |----------:|---------:|------:| +| Empty | --bool -s test | 63.58 ms | 0.825 ms | 0.83 | +| EmptyAOT | --bool -s test | 14.39 ms | 0.507 ms | 0.19 | +| SystemCommandLineBeta4 | --bool -s test | 85.80 ms | 1.007 ms | 1.12 | +| SystemCommandLineNow | --bool -s test | 76.74 ms | 1.099 ms | 1.00 | +| SystemCommandLineNowR2R | --bool -s test | 69.35 ms | 1.127 ms | 0.90 | +| SystemCommandLineNowAOT | --bool -s test | 17.35 ms | 0.487 ms | 0.23 | +``` + +As you can see, the startup time (the benchmarks report the time required to run given executable) has improved by 12% compared to beta4. If we compile the app with NativeAOT, it is just 3 ms slower than a NativeAOT app that does not parse the args at all (EmptyAOT in the table above). Also, when we exclude the overhead of an empty app (63.58 ms), the parsing is 40% faster than in beta4 (22.22 ms vs 13.66 ms). + +## See also + +- [System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/command-line-configuration.md b/docs/standard/commandline/command-line-configuration.md new file mode 100644 index 0000000000000..e9deb43fb7b0d --- /dev/null +++ b/docs/standard/commandline/command-line-configuration.md @@ -0,0 +1,55 @@ +--- +title: How to configure the parser in System.CommandLine +description: "Learn how to configure the parser in System.CommandLine." +ms.date: 16/06/2025 +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" +ms.topic: how-to +--- + +# How to configure the parser in System.CommandLine + +[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] + + is a class that provides properties to configure the parser. It is an optional argument for every `Parse` method, such as or . When it is not provided, the default configuration is used. + +Every instance has a property that returns the configuration used for parsing. + +## Standard output and error + + makes testing, as well as many extensibility scenarios, easier than using `System.Console`. It exposes two `TextWriter` properties: `Output` and `Error`. These can be set to any `TextWriter` instance, such as a `StringWriter`, which can be used to capture output for testing. + +Let's define a simple command that writes to standard output: + +:::code language="csharp" source="snippets/configuration/csharp/Program.cs" id="setaction"::: + +Now, let's use `CommandLineConfiguration` to capture the output: + +:::code language="csharp" source="snippets/configuration/csharp/Program.cs" id="captureoutput"::: + +## EnablePosixBundling + +[Bundling](syntax.md#option-bundling) of single-character options is enabled by default, but you can disable it by setting the property to `false`. + +## ProcessTerminationTimeout + +[Process termination timeout](parse-and-invoke.md#process-termination-timeout) can be configured via the property. The default value is 2 seconds. + +## ResponseFileTokenReplacer + +[Response files](syntax.md#response-files) are enabled by default, but you can disable them by setting the property to `null`. You can also provide a custom implementation to customize how response files are processed. + +## EnableDefaultExceptionHandler + +By default, all unhandled exceptions thrown during the invocation of a command are caught and reported to the user. This behavior can be disabled by setting the property to `false`. This is useful when you want to handle exceptions in a custom way, such as logging them or providing a different user experience. + +## Derived classes + + is not sealed, so you can derive from it to add custom properties or methods. This is useful when you want to provide additional configuration options specific to your application. + +## See also + +- [System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/customize-help.md b/docs/standard/commandline/customize-help.md deleted file mode 100644 index da517f1b8c1b5..0000000000000 --- a/docs/standard/commandline/customize-help.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: How to customize help in System.CommandLine -description: "Learn how to customize help in apps that are built with the System.Commandline library." -ms.date: 04/07/2022 -no-loc: [System.CommandLine] -helpviewer_keywords: - - "command line interface" - - "command line" - - "System.CommandLine" -ms.topic: how-to ---- -# How to customize help in apps that are built with the System.Commandline library - -You can customize help for a specific command, option, or argument, and you can add or replace whole help sections. - -The examples in this article work with the following command-line application: - -This code requires a `using` directive: - -```csharp -using System.CommandLine; -``` - -:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="original" ::: - -Without customization, the following help output is produced: - -```output -Description: - Read a file - -Usage: - scl [options] - -Options: - --file The file to print out. [default: scl.runtimeconfig.json] - --light-mode Determines whether the background color will be black or - white - --color Specifies the foreground color of console output - - --version Show version information - -?, -h, --help Show help and usage information -``` - -## Customize help for a single option or argument - -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] - -To customize the name of an option's argument, use the option's property. And lets you customize several parts of the help output for a command, option, or argument ( is the base class for all three types). With `CustomizeSymbol`, you can specify: - -* The first column text. -* The second column text. -* The way a default value is described. - -In the sample app, `--light-mode` is explained adequately, but changes to the `--file` and `--color` option descriptions will be helpful. For `--file`, the argument can be identified as a `` instead of ``. For the `--color` option, you can shorten the list of available colors in column one, and in column two you can add a warning that some colors won't work with some backgrounds. - -To make these changes, delete the `await rootCommand.InvokeAsync(args);` line shown in the preceding code and add in its place the following code: - -:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="first2columns" ::: - -The updated code requires additional `using` directives: - -```csharp -using System.CommandLine.Builder; -using System.CommandLine.Help; -using System.CommandLine.Parsing; -``` - -The app now produces the following help output: - -```output -Description: - Read a file - -Usage: - scl [options] - -Options: - --file The file to print out. [default: CustomHelp.runtimeconfig.json] - --light-mode Determines whether the background color will be black or white - --color Specifies the foreground color. Choose a color that provides enough contrast - with the background color. For example, a yellow foreground can't be read - against a light mode background. - --version Show version information - -?, -h, --help Show help and usage information -``` - -This output shows that the `firstColumnText` and `secondColumnText` parameters support word wrapping within their columns. - -## Add or replace help sections - -You can add or replace a whole section of the help output. For example, suppose you want to add some ASCII art to the description section by using the [Spectre.Console](https://www.nuget.org/packages/Spectre.Console/) NuGet package. - -Change the layout by adding a call to in the lambda passed to the method: - -:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="description" highlight="14-22" ::: - -The preceding code requires an additional `using` directive: - -```csharp -using Spectre.Console; -``` - -The class lets you reuse pieces of existing help formatting functionality and compose them into your custom help. - -The help output now looks like this: - -```output - ____ _ __ _ _ - | _ \ ___ __ _ __| | __ _ / _| (_) | | ___ - | |_) | / _ \ / _` | / _` | / _` | | |_ | | | | / _ \ - | _ < | __/ | (_| | | (_| | | (_| | | _| | | | | | __/ - |_| \_\ \___| \__,_| \__,_| \__,_| |_| |_| |_| \___| - - -Usage: - scl [options] - -Options: - --file The file to print out. [default: CustomHelp.runtimeconfig.json] - --light-mode Determines whether the background color will be black or white - --color Specifies the foreground color. Choose a color that provides enough contrast - with the background color. For example, a yellow foreground can't be read - against a light mode background. - --version Show version information - -?, -h, --help Show help and usage information -``` - -If you want to just use a string as the replacement section text instead of formatting it with `Spectre.Console`, replace the `Prepend` code in the preceding example with the following code: - -```csharp -.Prepend( - _ => _.Output.WriteLine("**New command description section**") -``` - -## See also - -[System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/define-commands.md b/docs/standard/commandline/define-commands.md deleted file mode 100644 index 2eea6861e7776..0000000000000 --- a/docs/standard/commandline/define-commands.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: How to define commands in System.CommandLine -description: "Learn how to define commands, options, and arguments by using the System.Commandline library." -ms.date: 04/07/2022 -no-loc: [System.CommandLine] -helpviewer_keywords: - - "command line interface" - - "command line" - - "System.CommandLine" -ms.topic: how-to ---- -# How to define commands, options, and arguments in System.CommandLine - -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] - -This article explains how to define [commands](syntax.md#commands), [options](syntax.md#options), and [arguments](syntax.md#arguments) in command-line apps that are built with the `System.CommandLine` library. To build a complete application that illustrates these techniques, see the tutorial [Get started with System.CommandLine](get-started-tutorial.md). - -For guidance on how to design a command-line app's commands, options, and arguments, see [Design guidance](syntax.md#design-guidance). - -## Define a root command - -Every command-line app has a [root command](syntax.md#root-commands), which refers to the executable file itself. The simplest case for invoking your code, if you have an app with no subcommands, options, or arguments, would look like this: - -:::code language="csharp" source="snippets/define-commands/csharp/Program.cs" id="all" ::: - -## Define subcommands - -Commands can have child commands, known as [*subcommands* or *verbs*](syntax.md#subcommands), and they can nest as many levels as you need. You can add subcommands as shown in the following example: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="definecommands" ::: - -The innermost subcommand in this example can be invoked like this: - -```console -myapp sub1 sub1a -``` - -## Define options - -A command handler method typically has parameters, and the values can come from command-line [options](syntax.md#options). The following example creates two options and adds them to the root command. The option names include double-hyphen prefixes, which is [typical for POSIX CLIs](syntax.md#options). The command handler code displays the values of those options: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="defineoptions" ::: - -Here's an example of command-line input and the resulting output for the preceding example code: - -```console -myapp --delay 21 --message "Hello world!" -``` - -```output ---delay = 21 ---message = Hello world! -``` - -### Global options - -To add an option to one command at a time, use the `Add` or `AddOption` method as shown in the preceding example. To add an option to a command and recursively to all of its subcommands, use the `AddGlobalOption` method, as shown in the following example: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="defineglobal" highlight="7" ::: - -The preceding code adds `--delay` as a global option to the root command, and it's available in the handler for `subCommand1a`. - -## Define arguments - -[Arguments](syntax.md#arguments) are defined and added to commands like options. The following example is like the options example, but it defines arguments instead of options: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="definearguments" ::: - -Here's an example of command-line input and the resulting output for the preceding example code: - -```console -myapp 42 "Hello world!" -``` - -```output - argument = 42 - argument = Hello world! -``` - -An argument that is defined without a default value, such as `messageArgument` in the preceding example, is treated as a required argument. An error message is displayed, and the command handler isn't called, if a required argument isn't provided. - -## Define aliases - -Both commands and options support [aliases](syntax.md#aliases). You can add an alias to an option by calling `AddAlias`: - -```csharp -var option = new Option("--framework"); -option.AddAlias("-f"); -``` - -Given this alias, the following command lines are equivalent: - -```console -myapp -f net6.0 -myapp --framework net6.0 -``` - -Command aliases work the same way. - -```csharp -var command = new Command("serialize"); -command.AddAlias("serialise"); -``` - -This code makes the following command lines equivalent: - -```console -myapp serialize -myapp serialise -``` - -We recommend that you minimize the number of option aliases that you define, and avoid defining certain aliases in particular. For more information, see [Short-form aliases](syntax.md#short-form-aliases). - -## Required options - -To make an option required, set its `IsRequired` property to `true`, as shown in the following example: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="requiredoption" ::: - -The options section of the command help indicates the option is required: - -```output -Options: - --endpoint (REQUIRED) - --version Show version information - -?, -h, --help Show help and usage information -``` - -If the command line for this example app doesn't include `--endpoint`, an error message is displayed and the command handler isn't called: - -```output -Option '--endpoint' is required. -``` - -If a required option has a default value, the option doesn't have to be specified on the command line. In that case, the default value provides the required option value. - -## Hidden commands, options, and arguments - -You might want to support a command, option, or argument, but avoid making it easy to discover. For example, it might be a deprecated or administrative or preview feature. Use the property to prevent users from discovering such features by using tab completion or help, as shown in the following example: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="hiddenoption" ::: - -The options section of this example's command help omits the `--endpoint` option. - -```output -Options: - --version Show version information - -?, -h, --help Show help and usage information -``` - -## Set argument arity - -You can explicitly set argument [arity](syntax.md#argument-arity) by using the `Arity` property, but in most cases that is not necessary. `System.CommandLine` automatically determines the argument arity based on the argument type: - -| Argument type | Default arity | -|------------------|----------------------------| -| `Boolean` | `ArgumentArity.ZeroOrOne` | -| Collection types | `ArgumentArity.ZeroOrMore` | -| Everything else | `ArgumentArity.ExactlyOne` | - -## Multiple arguments - -By default, when you call a command, you can repeat an option name to specify multiple arguments for an option that has maximum [arity](syntax.md#argument-arity) greater than one. - -```console -myapp --items one --items two --items three -``` - -To allow multiple arguments without repeating the option name, set to `true`. This setting lets you enter the following command line. - -```console -myapp --items one two three -``` - -The same setting has a different effect if maximum argument arity is 1. It allows you to repeat an option but takes only the last value on the line. In the following example, the value `three` would be passed to the app. - -```console -myapp --item one --item two --item three -``` - -## List valid argument values - -To specify a list of valid values for an option or argument, specify an enum as the option type or use , as shown in the following example: - -:::code language="csharp" source="snippets/define-commands/csharp/Program2.cs" id="staticlist" ::: - -Here's an example of command-line input and the resulting output for the preceding example code: - -```console -myapp --language not-a-language -``` - -```output -Argument 'not-a-language' not recognized. Must be one of: - 'csharp' - 'fsharp' - 'vb' - 'pwsh' - 'sql' -``` - -The options section of command help shows the valid values: - -```output -Options: - --language An option that must be one of the values of a static list. - --version Show version information - -?, -h, --help Show help and usage information -``` - -## Option and argument validation - -For information about argument validation and how to customize it, see the following sections in the [Parameter binding](model-binding.md) article: - -* [Built-in type and arity argument validation](model-binding.md#built-in-argument-validation) -* [Custom validation and binding](model-binding.md#custom-validation-and-binding) - -## See also - -* [System.CommandLine overview](index.md) -* [Parameter binding](model-binding.md) diff --git a/docs/standard/commandline/dependency-injection.md b/docs/standard/commandline/dependency-injection.md deleted file mode 100644 index ef4b3d9134829..0000000000000 --- a/docs/standard/commandline/dependency-injection.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: How to configure dependency injection in System.CommandLine -description: "Learn how to configure dependency injection in System.CommandLine." -ms.date: 05/22/2022 -no-loc: [System.CommandLine] -helpviewer_keywords: - - "command line interface" - - "command line" - - "System.CommandLine" -ms.topic: how-to ---- -# How to configure dependency injection in System.CommandLine - -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] - -Use a [custom binder](model-binding.md#parameter-binding-more-than-8-options-and-arguments) to inject custom types into a command handler. - -We recommend handler-specific dependency injection (DI) for the following reasons: - -* Command-line apps are often short-lived processes, in which startup cost can have a noticeable impact on performance. Optimizing performance is particularly important when tab completions have to be calculated. Command-line apps are unlike Web and GUI apps, which tend to be relatively long-lived processes. Unnecessary startup time is not appropriate for short-lived processes. -* When a command-line app that has multiple subcommands is run, only one of those subcommands will be executed. If an app configures dependencies for the subcommands that don't run, it needlessly degrades performance. - -To configure DI, create a class that derives from where `T` is the interface that you want to inject an instance for. In the method override, get and return the instance you want to inject. The following example injects the default logger implementation for : - -:::code language="csharp" source="snippets/dependency-injection/csharp/Program.cs" id="binderclass" ::: - -When calling the method, pass to the lambda an instance of the injected class and pass an instance of your binder class in the list of services: - -:::code language="csharp" source="snippets/dependency-injection/csharp/Program.cs" id="sethandler" ::: - -The following code is a complete program that contains the preceding examples: - -:::code language="csharp" source="snippets/dependency-injection/csharp/Program.cs" id="all" ::: - -## See also - -[System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/design-guidance.md b/docs/standard/commandline/design-guidance.md new file mode 100644 index 0000000000000..3b1d300c79931 --- /dev/null +++ b/docs/standard/commandline/design-guidance.md @@ -0,0 +1,167 @@ +--- +title: Command-line design guidance for System.CommandLine +description: "Provides guidance for designing a command-line interface." +ms.date: 06/16/2025 +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line" + - "System.CommandLine" +ms.topic: conceptual +--- + +# Design guidance + +The following sections present guidance that we recommend you follow when designing a CLI. Think of what your app expects on the command line as similar to what a REST API server expects in the URL. Consistent rules for REST APIs are what make them readily usable to client app developers. In the same way, users of your command-line apps will have a better experience if the CLI design follows common patterns. + +Once you create a CLI, it is hard to change, especially if your users have used your CLI in scripts they expect to keep running. The guidelines here were developed after the .NET CLI, and it doesn't always follow these guidelines. We are updating the .NET CLI where we can do so without introducing breaking changes. An example of this work is the new design for `dotnet new` in .NET 7. + +## Symbols + +### Commands and subcommands + +If a command has subcommands, the command should function as an area or a grouping identifier for the subcommands, rather than specify an action. When you invoke the app, you specify the grouping command and one of its subcommands. For example, try to run `dotnet tool`, and you get an error message because the `tool` command only identifies a group of tool-related subcommands, such as `install` and `list`. You can run `dotnet tool install`, but `dotnet tool` by itself would be incomplete. + +One of the ways that defining areas helps your users is that it organizes the help output. + +Within a CLI, there is often an implicit area. For example, in the .NET CLI, the implicit area is the project, and in the Docker CLI, it is the image. As a result, you can use `dotnet build` without including an area. Consider whether your CLI has an implicit area. If it does, consider whether to allow the user to optionally include or omit it, as in `docker build` and `docker image build`. If you optionally allow the implicit area to be typed by your user, you also automatically have help and tab completion for this grouping of commands. Supply the optional use of the implicit group by defining two commands that perform the same operation. + +### Options as parameters + +Options should provide parameters to commands, rather than specifying actions themselves. This is a recommended design principle, although it isn't always followed by `System.CommandLine` (`--help` displays help information). + +## Naming + +### Short-form aliases + +In general, we recommend that you minimize the number of short-form option aliases that you define. + +In particular, avoid using any of the following aliases differently than their common usage in the .NET CLI and other .NET command-line apps: + +* `-i` for `--interactive`. + + This option signals to the user that they may be prompted for inputs to questions that the command needs answered. For example, prompting for a username. Your CLI may be used in scripts, so use caution in prompting users who have not specified this switch. + +* `-o` for `--output`. + + Some commands produce files as the result of their execution. This option should be used to help determine where those files should be located. In cases where a single file is created, this option should be a file path. In cases where many files are created, this option should be a directory path. + +* `-v` for `--verbosity`. + + Commands often provide output to the user on the console; this option is used to specify the amount of output the user requests. For more information, see [The `--verbosity` option](#the---verbosity-option) later in this article. + +There are also some aliases with common usage limited to the .NET CLI. You can use these aliases for other options in your apps, but be aware of the possibility of confusion. + +* `-c` for `--configuration` + + This option often refers to a named Build Configuration, like `Debug` or `Release`. You can use any name you want for a configuration, but most tools are expecting one of those. This setting is often used to configure other properties in a way that makes sense for that configuration—for example, doing less code optimization when building the `Debug` configuration. Consider this option if your command has different modes of operation. + +* `-f` for `--framework` + + This option is used to select a single [Target Framework Moniker (TFM)](../frameworks.md) to execute for, so if your CLI application has differing behavior based on which TFM is chosen, you should support this flag. + +* `-p` for `--property` + + If your application eventually invokes MSBuild, the user will often need to customize that call in some way. This option allows for MSBuild properties to be provided on the command line and passed on to the underlying MSBuild call. If your app doesn't use MSBuild but needs a set of key-value pairs, consider using this same option name to take advantage of users' expectations. + +* `-r` for `--runtime` + + If your application can run on different runtimes, or has runtime-specific logic, consider supporting this option as a way of specifying a [Runtime Identifier](../../core/rid-catalog.md). If your app supports `--runtime`, consider supporting `--os` and `--arch` also. These options let you specify just the OS or the architecture parts of the RID, leaving the part not specified to be determined from the current platform. For more information, see [dotnet publish](../../core/tools/dotnet-publish.md). + +### Short names + +Make names for commands, options, and arguments as short and easy to spell as possible. For example, if `class` is clear enough, don't make the command `classification`. + +### Lowercase names + +Define names in lowercase only, except you can make uppercase aliases to make commands or options case-insensitive. + +### Kebab case names + +Use [kebab case](https://en.wikipedia.org/wiki/Letter_case#Kebab_case) to distinguish words. For example, `--additional-probing-path`. + +### Pluralization + +Within an app, be consistent in pluralization. For example, don't mix plural and singular names for options that can have multiple values (maximum arity greater than one): + +| Option names | Consistency | +|----------------------------------------------|--------------| +| `--additional-probing-paths` and `--sources` | ✔️ | +| `--additional-probing-path` and `--source` | ✔️ | +| `--additional-probing-paths` and `--source` | ❌ | +| `--additional-probing-path` and `--sources` | ❌ | + +### Verbs vs. nouns + +Use verbs rather than nouns for commands that refer to actions (those without subcommands under them), for example: `dotnet workload remove`, not `dotnet workload removal`. And use nouns rather than verbs for options, for example: `--configuration`, not `--configure`. + +## The `--verbosity` option + +`System.CommandLine` applications typically offer a `--verbosity` option that specifies how much output is sent to the console. Here are the standard five settings: + +* `Q[uiet]` +* `M[inimal]` +* `N[ormal]` +* `D[etailed]` +* `Diag[nostic]` + +These are the standard names, but existing apps sometimes use `Silent` in place of `Quiet`, and `Trace`, `Debug`, or `Verbose` in place of `Diagnostic`. + +Each app defines its own criteria that determine what gets displayed at each level. Typically, an app only needs three levels: + +* Quiet +* Normal +* Diagnostic + +If an app doesn't need five different levels, the option should still define the same five settings. In that case, `Minimal` and `Normal` will produce the same output, and `Detailed` and `Diagnostic` will likewise be the same. This allows your users to just type what they are familiar with, and the best fit will be used. + +The expectation for `Quiet` is that no output is displayed on the console. However, if an app offers an interactive mode, the app should do one of the following: + +* Display prompts for input when `--interactive` is specified, even if `--verbosity` is `Quiet`. +* Disallow the use of `--verbosity Quiet` and `--interactive` together. + +Otherwise, the app will wait for input without telling the user what it's waiting for. It will appear that your application froze, and the user will have no idea the application is waiting for input. + +If you define aliases, use `-v` for `--verbosity` and make `-v` without an argument an alias for `--verbosity Diagnostic`. Use `-q` for `--verbosity Quiet`. + +## The .NET CLI and POSIX conventions + +The .NET CLI does not consistently follow all POSIX conventions. + +### Double-dash + +Several commands in the .NET CLI have a special implementation of the double-dash token. In the case of `dotnet run`, `dotnet watch`, and `dotnet tool run`, tokens that follow `--` are passed to the app that is being run by the command. For example: + +```dotnetcli +dotnet run --project ./myapp.csproj -- --message "Hello world!" + ^^ +``` + +In this example, the `--project` option is passed to the `dotnet run` command, and the `--message` option with its argument is passed as a command-line option to *myapp* when it runs. + +The `--` token is not always required for passing options to an app that you run by using `dotnet run`. Without the double-dash, the `dotnet run` command automatically passes on to the app being run any options that aren't recognized as applying to `dotnet run` itself or to MSBuild. So the following command lines are equivalent because `dotnet run` doesn't recognize the arguments and options: + +```dotnetcli +dotnet run -- quotes read --delay 0 --fg-color red +dotnet run quotes read --delay 0 --fg-color red +``` + +### Omission of the option-to-argument delimiter + +The .NET CLI doesn't support the POSIX convention that lets you omit the delimiter when you are specifying a single-character option alias. + +### Multiple arguments without repeating the option name + +The .NET CLI doesn't accept multiple arguments for one option without repeating the option name. + +### Boolean options + +In the .NET CLI, some Boolean options result in the same behavior when you pass `false` as when you pass `true`. This behavior results when .NET CLI code that implements the option only checks for the presence or absence of the option, ignoring the value. An example is `--no-restore` for the `dotnet build` command. Pass `--no-restore false` and the restore operation will be skipped the same as when you specify `--no-restore true` or `--no-restore`. + +### Kebab case + +In some cases, the .NET CLI doesn't use kebab case for command, option, or argument names. For example, there is a .NET CLI option that is named [`--additionalprobingpath`](../../core/tools/dotnet.md#additionalprobingpath) instead of `--additional-probing-path`. + +## See also + +* [Open-source CLI design guidance](https://clig.dev/) +* [GNU standards](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html) diff --git a/docs/standard/commandline/get-started-tutorial.md b/docs/standard/commandline/get-started-tutorial.md index f79887641e068..e4c5f0231f14d 100644 --- a/docs/standard/commandline/get-started-tutorial.md +++ b/docs/standard/commandline/get-started-tutorial.md @@ -13,7 +13,7 @@ helpviewer_keywords: [!INCLUDE [scl-preview](../../../includes/scl-preview.md)] -This tutorial shows how to create a .NET command-line app that uses the [`System.CommandLine` library](index.md). You'll begin by creating a simple root command that has one option. Then you'll add to that base, creating a more complex app that contains multiple subcommands and different options for each command. +This tutorial shows how to create a .NET command-line app that uses the [`System.CommandLine` library](index.md). You'll begin by creating a simple root command that has one option. Then you'll build on that base, creating a more complex app that contains multiple subcommands and different options for each command. In this tutorial, you learn how to: @@ -25,8 +25,8 @@ In this tutorial, you learn how to: > * Assign an option recursively to all subcommands under a command. > * Work with multiple levels of nested subcommands. > * Create aliases for commands and options. -> * Work with `string`, `string[]`, `int`, `bool`, `FileInfo` and enum option types. -> * Bind option values to command handler code. +> * Work with `string`, `string[]`, `int`, `bool`, `FileInfo`, and enum option types. +> * Read option values in command action code. > * Use custom code for parsing and validating options. ## Prerequisites @@ -39,17 +39,17 @@ Or ## Create the app -Create a .NET 6 console app project named "scl". +Create a .NET 9 console app project named "scl". 1. Create a folder named *scl* for the project, and then open a command prompt in the new folder. 1. Run the following command: ```dotnetcli - dotnet new console --framework net6.0 + dotnet new console --framework net9.0 ``` -## Install the System.CommandLine package +### Install the System.CommandLine package * Run the following command: @@ -65,60 +65,48 @@ Create a .NET 6 console app project named "scl". The `--prerelease` option is necessary because the library is still in beta. +### Parse the arguments + 1. Replace the contents of *Program.cs* with the following code: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="all" ::: + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage0/Program.cs" id="all" ::: The preceding code: -* Creates an [option](syntax.md#options) named `--file` of type and assigns it to the [root command](syntax.md#commands): +* Creates an [option](syntax.md#options) named `--file` of type and adds it to the [root command](syntax.md#root-command): + + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="symbols" ::: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="option" ::: +* Parses the `args` and checks whether any value was provided for `--file` option. If so, it calls the `ReadFile` method using parsed value and returns `0` exit code: -* Specifies that `ReadFile` is the method that will be called when the root command is invoked: +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage0/Program.cs" id="parse" ::: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="sethandler" ::: +* If no value was provided for `--file`, it prints available parse errors and returns `1` exit code: -* Displays the contents of the specified file when the root command is invoked: +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage0/Program.cs" id="errors" ::: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="handler" ::: +* The `ReadFile` method reads the specified file and displays its contents on the console: + +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage0/Program.cs" id="action" ::: ## Test the app You can use any of the following ways to test while developing a command-line app: -* Run the `dotnet build` command, and then open a command prompt in the *scl/bin/Debug/net6.0* folder to run the executable: +* Run the `dotnet build` command, and then open a command prompt in the *scl/bin/Debug/net9.0* folder to run the executable: ```console dotnet build - cd bin/Debug/net6.0 + cd bin/Debug/net9.0 scl --file scl.runtimeconfig.json ``` * Use `dotnet run` and pass option values to the app instead of to the `run` command by including them after `--`, as in the following example: ```dotnetcli - dotnet run -- --file bin/Debug/net6.0/scl.runtimeconfig.json - ``` - -The working directory is the project folder (the folder that has the .csproj file), so the relative path to `scl.runtimeconfig.json` is from the project folder. - - In .NET 7.0.100 SDK Preview, you can use the `commandLineArgs` of a *launchSettings.json* file by running the command `dotnet run --launch-profile `. - -* [Publish the project to a folder](../../core/tutorials/publishing-with-visual-studio-code.md), open a command prompt to that folder, and run the executable: - - ```console - dotnet publish -o publish - cd ./publish - scl --file scl.runtimeconfig.json + dotnet run -- --file bin/Debug/net9.0/scl.runtimeconfig.json ``` -* In Visual Studio 2022, select **Debug** > **Debug Properties** from the menu, and enter the options and arguments in the **Command line arguments** box. For example: - - :::image type="content" source="media/get-started-tutorial/cmd-line-args.png" alt-text="Command line arguments in Visual Studio 2022"::: - - Then run the app, for example by pressing Ctrl+F5. - This tutorial assumes you're using the first of these options. When you run the app, it displays the contents of the file specified by the `--file` option. @@ -126,23 +114,45 @@ When you run the app, it displays the contents of the file specified by the `--f ```output { "runtimeOptions": { - "tfm": "net6.0", + "tfm": "net9.0", "framework": { "name": "Microsoft.NETCore.App", - "version": "6.0.0" + "version": "9.0.0" } } } ``` -### Help output +But what happens if you ask it to display the help by providing `--help`? Nothing gets printed to the console, because the app doesn't yet handle a scenario, where `--file` is not provided and there are no parse errors. + +## Parse the arguments and invoke the ParseResult + +System.CommandLine allows the users to specify an action that is invoked when given symbol (command, directive or option) is parsed successfully. The action is a delegate that takes a parameter and returns an `int` exit code (async actions are also [available](parse-and-invoke.md#asynchronous-actions)). The exit code is returned by the method and can be used to indicate whether the command was executed successfully or not. + +1. Replace the contents of *Program.cs* with the following code: + + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="all" ::: + +The preceding code: + +* Specifies that `ReadFile` is the method that will be called when the root command is **invoked**: + + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="setaction" ::: + +* Parses the `args` and **invokes** the result: + + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="invoke" ::: + +When you run the app, it displays the contents of the file specified by the `--file` option. -`System.CommandLine` automatically provides help output: +What happens if you ask it to display the help by providing `--help`? ```console scl --help ``` +Following output gets printed: + ```output Description: Sample app for System.CommandLine @@ -151,21 +161,20 @@ Usage: scl [options] Options: - --file The file to read and display on the console. - --version Show version information -?, -h, --help Show help and usage information + --version Show version information + --file The file to read and display on the conso ``` -### Version output - -`System.CommandLine` automatically provides version output: + by default provides [Help option](help.md#help-option), [Version option](syntax.md#version-option) and [Suggest directive](syntax.md#suggest-directive). `ParseResult.Invoke` method is responsible for invoking the action of parsed symbol. It could be the action explicitly defined for our command, or the help action defined by `System.CommandLine` for . Moreover, when it detects any parse errors, it prints them to the standard error, prints help to standard output and returns `1` exit code: ```console -scl --version +scl --invalid bla ``` ```output -1.0.0 +Unrecognized command or argument '--invalid'. +Unrecognized command or argument 'bla'. ``` ## Add a subcommand and options @@ -190,35 +199,33 @@ The new options will let you configure the foreground and background text colors ``` - Adding this markup causes the text file to be copied to the *bin/debug/net6.0* folder when you build the app. So when you run the executable in that folder, you can access the file by name without specifying a folder path. + Adding this markup causes the text file to be copied to the *bin/debug/net9.0* folder when you build the app. So when you run the executable in that folder, you can access the file by name without specifying a folder path. 1. In *Program.cs*, after the code that creates the `--file` option, create options to control the readout speed and text colors: :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="options" ::: -1. After the line that creates the root command, delete the line that adds the `--file` option to it. You're removing it here because you'll add it to a new subcommand. +1. After the line that creates the root command, delete the code that adds the `--file` option to it. You're removing it here because you'll add it to a new subcommand. - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="rootcommand" ::: - -1. After the line that creates the root command, create a `read` subcommand. Add the options to this subcommand, and add the subcommand to the root command. +1. After the line that creates the root command, create a `read` subcommand. Add the options to this subcommand (by using collection initializer syntax rather than `Options` property), and add the subcommand to the root command. :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="subcommand" ::: -1. Replace the `SetHandler` code with the following `SetHandler` code for the new subcommand: +1. Replace the `SetAction` code with the following `SetAction` code for the new subcommand: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="sethandler" ::: + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="setaction" ::: - You're no longer calling `SetHandler` on the root command because the root command no longer needs a handler. When a command has subcommands, you typically have to specify one of the subcommands when invoking a command-line app. + You're no longer calling `SetAction` on the root command because the root command no longer needs an action. When a command has subcommands, you typically have to specify one of the subcommands when invoking a command-line app. -1. Replace the `ReadFile` handler method with the following code: +1. Replace the `ReadFile` action method with the following code: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="handler" ::: + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="action" ::: The app now looks like this: :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage2/Program.cs" id="all" ::: -## Test the new subcommand +### Test the new subcommand Now if you try to run the app without specifying the subcommand, you get an error message followed by a help message that specifies the subcommand that is available. @@ -229,6 +236,7 @@ scl --file sampleQuotes.txt ```output '--file' was not matched. Did you mean one of the following? --help + Required command was not provided. Unrecognized command or argument '--file'. Unrecognized command or argument 'sampleQuotes.txt'. @@ -240,8 +248,8 @@ Usage: scl [command] [options] Options: - --version Show version information -?, -h, --help Show help and usage information + --version Show version information Commands: read Read and display the file. @@ -308,21 +316,21 @@ scl read --file nofile ``` ```output -Unhandled exception: System.IO.FileNotFoundException: -Could not find file 'C:\bin\Debug\net6.0\nofile'. +Unhandled exception: System.IO.FileNotFoundException: Could not find file 'C:\bin\Debug\net9.0\nofile''. +File name: 'C:\bin\Debug\net9.0\nofile'' ``` ## Add subcommands and custom validation This section creates the final version of the app. When finished, the app will have the following commands and options: -* root command with a global\* option named `--file` +* root command with a recursive\* option named `--file` * `quotes` command * `read` command with options named `--delay`, `--fgcolor`, and `--light-mode` * `add` command with arguments named `quote` and `byline` * `delete` command with option named `--search-terms` -\* A global option is available to the command it's assigned to and recursively to all its subcommands. +\* A recursive option is available to the command it's assigned to and recursively to all its subcommands. Here's sample command line input that invokes each of the available commands with its options and arguments: @@ -336,11 +344,11 @@ scl quotes delete --search-terms David "You can do" Antoine "Perfection is achie :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage3/Program.cs" id="fileoption" ::: - This code uses to provide custom parsing, validation, and error handling. + This code uses to provide custom parsing, validation, and error handling. Without this code, missing files are reported with an exception and stack trace. With this code just the specified error message is displayed. - This code also specifies a default value, which is why it sets `isDefault` to `true`. If you don't set `isDefault` to `true`, the `parseArgument` delegate doesn't get called when no input is provided for `--file`. + This code also specifies a default value, which is why it sets `DefaultValueFactory` to custom parsing method. 1. After the code that creates `lightModeOption`, add options and arguments for the `add` and `delete` commands: @@ -360,7 +368,7 @@ scl quotes delete --search-terms David "You can do" Antoine "Perfection is achie This code makes the following changes: * Removes the `--file` option from the `read` command. - * Adds the `--file` option as a global option to the root command. + * Adds the `--file` option as a recursive option to the root command. * Creates a `quotes` command and adds it to the root command. * Adds the `read` command to the `quotes` command instead of to the root command. @@ -374,19 +382,19 @@ scl quotes delete --search-terms David "You can do" Antoine "Perfection is achie * `add` * `delete` - The app now implements the recommended pattern where the parent command (`quotes`) specifies an area or group, and its children commands (`read`, `add`, `delete`) are actions. + The app now implements the [recommended](design-guidance.md#symbols) pattern where the parent command (`quotes`) specifies an area or group, and its children commands (`read`, `add`, `delete`) are actions. - Global options are applied to the command and recursively to subcommands. Since `--file` is on the root command, it will be available automatically in all subcommands of the app. + Recursive options are applied to the command and recursively to subcommands. Since `--file` is on the root command, it will be available automatically in all subcommands of the app. -1. After the `SetHandler` code, add new `SetHandler` code for the new subcommands: +1. After the `SetAction` code, add new `SetAction` code for the new subcommands: - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage3/Program.cs" id="sethandlers" ::: + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage3/Program.cs" id="setactions" ::: - Subcommand `quotes` doesn't have a handler because it isn't a leaf command. Subcommands `read`, `add`, and `delete` are leaf commands under `quotes`, and `SetHandler` is called for each of them. + Subcommand `quotes` doesn't have an action because it isn't a leaf command. Subcommands `read`, `add`, and `delete` are leaf commands under `quotes`, and `SetAction` is called for each of them. -1. Add the handlers for `add` and `delete`. +1. Add the actions for `add` and `delete`. - :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage3/Program.cs" id="handlers" ::: + :::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage3/Program.cs" id="actions" ::: The finished app looks like this: @@ -442,7 +450,7 @@ scl quotes delete --search-terms David "You can do" Antoine "Perfection is achie ``` > [!NOTE] -> If you're running in the *bin/debug/net6.0* folder, that folder is where you'll find the file with changes from the `add` and `delete` commands. The copy of the file in the project folder remains unchanged. +> If you're running in the *bin/debug/net9.0* folder, that folder is where you'll find the file with changes from the `add` and `delete` commands. The copy of the file in the project folder remains unchanged. ## Next steps diff --git a/docs/standard/commandline/handle-termination.md b/docs/standard/commandline/handle-termination.md deleted file mode 100644 index fc4229fca1568..0000000000000 --- a/docs/standard/commandline/handle-termination.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: How to handle termination in System.CommandLine -description: "Learn how to handle termination in apps that are built with the System.Commandline library." -ms.date: 05/24/2022 -no-loc: [System.CommandLine] -helpviewer_keywords: - - "command line interface" - - "command line" - - "System.CommandLine" -ms.topic: how-to ---- -# How to handle termination in System.CommandLine - -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] - -To handle termination, inject a instance into your handler code. This token can then be passed along to async APIs that you call from within your handler, as shown in the following example: - -:::code language="csharp" source="snippets/handle-termination/csharp/Program.cs" id="mainandhandler" ::: - -The preceding code uses a `SetHandler` overload that gets an [InvocationContext](model-binding.md#invocationcontext) instance rather than one or more `IValueDescriptor` objects. The `InvocationContext` is used to get the `CancellationToken` and [ParseResult](model-binding.md#parseresult) objects. `ParseResult` can provide argument or option values. - -To test the sample code, run the command with a URL that will take a moment to load, and before it finishes loading, press Ctrl+C. On macOS press Command+Period(.). For example: - -```dotnetcli -testapp --url https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis -``` - -```output -The operation was aborted -``` - -Cancellation actions can also be added directly using the method. - -For information about an alternative way to set the process exit code, see [Set exit codes](model-binding.md#set-exit-codes). - -## See also - -[System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/help.md b/docs/standard/commandline/help.md new file mode 100644 index 0000000000000..6ad0fc2e4c69f --- /dev/null +++ b/docs/standard/commandline/help.md @@ -0,0 +1,118 @@ +--- +title: How to customize help in System.CommandLine +description: "Learn how to use and customize help in apps that are built with the System.Commandline library." +ms.date: 04/07/2022 +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" +ms.topic: how-to +--- + +## Help option + +Command-line apps typically provide an option to display a brief description of the available commands, options, and arguments. `System.CommandLine` provides that is by default included in the [RootCommand](syntax.md#root-command) options. generates help output for defined symbols by using the information exposed by , , , and other properties like default value or completion sources. + +:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="original" ::: + +```dotnetcli +Description: + Read a file + +Usage: + scl [options] + +Options: + -?, -h, --help Show help and usage information + --version Show version information + --file The file to print out. + --light-mode Determines whether the background color will be black + or white + --color Specifies the foreground color of console output + +``` + +App users might be accustomed to different ways to request help on different platforms, so apps built on `System.CommandLine` respond to many ways of requesting help. The following commands are all equivalent: + +```dotnetcli +dotnet --help +dotnet -h +dotnet /h +dotnet -? +dotnet /? +``` + +Help output doesn't necessarily show all available commands, arguments, and options. Some of them may be *hidden* via the property, which means they don't show up in help output (and completions) but they can be specified on the command line. + +## Help customization + + You can customize help output for commands by defining specific help text for each symbol, providing further clarity to users regarding their usage. + +To customize the name of an option's argument, use the option's property. + +In the sample app, `--light-mode` is explained adequately, but changes to the `--file` and `--color` option descriptions will be helpful. For `--file`, the argument can be identified as a ``. For the `--color` option, you can shorten the list of available colors. + +To make these changes, extend the previous code with the following code: + +:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="allowedvalues" ::: + +The app now produces the following help output: + +```output +Description: + Read a file + +Usage: + scl [options] + +Options: + -?, -h, --help Show help and usage information + --version Show version information + --file The file to print out. + --light-mode Determines whether the background color will be black or white + --color Specifies the foreground color of console output [default: White] +``` + +## Add sections to help output + +You can add first or last sections to the help output. For example, suppose you want to add some ASCII art to the description section by using the [Spectre.Console](https://www.nuget.org/packages/Spectre.Console/) NuGet package. + +Define a custom action that performs some extra logic before and after calling the default `HelpAction`: + +:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="customaction" ::: + +Update the `HelpAction` defined by `RootCommand` to use the custom action: + +:::code language="csharp" source="snippets/customize-help/csharp/Program.cs" id="setcustomaction" highlight="5" ::: + +The help output now looks like this: + +```output + ____ _ __ _ _ + | _ \ ___ __ _ __| | __ _ / _| (_) | | ___ + | |_) | / _ \ / _` | / _` | / _` | | |_ | | | | / _ \ + | _ < | __/ | (_| | | (_| | | (_| | | _| | | | | | __/ + |_| \_\ \___| \__,_| \__,_| \__,_| |_| |_| |_| \___| + +Description: + Read a file + +Usage: + scl [options] + +Options: + -?, -h, --help Show help and usage information + --version Show version information + --file The file to print out. + --light-mode Determines whether the background color will be black or white + --color Specifies the foreground color of console output [default: White] + +Sample usage: --file input.txt +``` + +## See also + +[System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/index.md b/docs/standard/commandline/index.md index 3d8c1136adeff..56219eae58854 100644 --- a/docs/standard/commandline/index.md +++ b/docs/standard/commandline/index.md @@ -1,6 +1,6 @@ --- title: System.CommandLine overview -description: "Learn how to develop and use command-line apps that are based on the System.CommandLine library" +description: "Learn how to develop and use command-line apps that are based on the System.CommandLine library." ms.date: 04/07/2022 no-loc: [System.CommandLine] helpviewer_keywords: @@ -10,45 +10,45 @@ helpviewer_keywords: ms.topic: overview --- -# System.CommandLine overview +# System.CommandLine Overview [!INCLUDE [scl-preview](../../../includes/scl-preview.md)] -The `System.CommandLine` library provides functionality that is commonly needed by command-line apps, such as parsing the command-line input and displaying help text. +The `System.CommandLine` library provides functionality commonly needed by command-line apps, such as parsing command-line input and displaying help text. Apps that use `System.CommandLine` include the [.NET CLI](../../core/tools/index.md), [additional tools](../../core/additional-tools/index.md), and many [global and local tools](../../core/tools/global-tools.md). For app developers, the library: -* Lets you focus on writing your app code, since you don't have to write code to parse command-line input or produce a help page. -* Lets you test app code independently of input parsing code. -* Is [trim-friendly](../../core/deploying/trimming/trim-self-contained.md), making it a good choice for developing a fast, lightweight, AOT-capable CLI app. +- Lets you focus on writing your app code, since you don't have to write code to parse command-line input or produce a help page. +- Lets you test app code independently of input parsing code. +- Is [trim-friendly](../../core/deploying/trimming/trim-self-contained.md), making it a good choice for developing fast, lightweight, AOT-capable CLI apps. Use of the library also benefits app users: -* It ensures that command-line input is parsed consistently according to [POSIX](https://en.wikipedia.org/wiki/POSIX) or Windows conventions. -* It automatically supports [tab completion](tab-completion.md) and [response files](syntax.md#response-files). +- It ensures that command-line input is parsed consistently according to [POSIX](https://en.wikipedia.org/wiki/POSIX) or Windows conventions. +- It automatically supports [tab completion](tab-completion.md) and [response files](syntax.md#response-files). -## NuGet package +## NuGet Package -The library is available in a NuGet package: +The library is available as a NuGet package: -* [System.CommandLine](https://www.nuget.org/packages/System.CommandLine) +- [System.CommandLine](https://www.nuget.org/packages/System.CommandLine) -## Next steps +## Next Steps To get started with System.CommandLine, see the following resources: -* [Tutorial: Get started with System.CommandLine](get-started-tutorial.md) -* [Command-line syntax overview](syntax.md) +- [Tutorial: Get started with System.CommandLine](get-started-tutorial.md) +- [Syntax overview: commands, options, and arguments](syntax.md) To learn more, see the following resources: -* [How to define commands, options, and arguments](define-commands.md) -* [How to bind arguments to handlers](model-binding.md) -* [How to configure dependency injection](dependency-injection.md) -* [How to enable and customize tab completion](tab-completion.md) -* [How to customize help](customize-help.md) -* [How to handle termination](handle-termination.md) -* [How to write middleware and directives](use-middleware.md) -* [System.CommandLine API reference](xref:System.CommandLine) +- [How to parse and invoke the result](parse-and-invoke.md) +- [How to customize parsing and validation](parsing-and-validation.md) +- [How to configure the parser](command-line-configuration.md) +- [How to customize help](help.md) +- [How to enable and customize tab completion](tab-completion.md) +- [Command-line design guidance](design-guidance.md) +- [Breaking changes in beta5](beta5.md) +- [System.CommandLine API reference](xref:System.CommandLine) diff --git a/docs/standard/commandline/model-binding.md b/docs/standard/commandline/model-binding.md deleted file mode 100644 index e736a3b540f39..0000000000000 --- a/docs/standard/commandline/model-binding.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -title: How to bind arguments to handlers in System.CommandLine -description: "Learn how to do model-binding in apps that are built with the System.Commandline library." -ms.date: 05/24/2022 -no-loc: [System.CommandLine] -helpviewer_keywords: - - "command line interface" - - "command line" - - "System.CommandLine" -ms.topic: how-to ---- -# How to bind arguments to handlers in System.CommandLine - -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] - -The process of parsing arguments and providing them to command handler code is called *parameter binding*. `System.CommandLine` has the ability to bind many argument types built in. For example, integers, enums, and file system objects such as and can be bound. Several `System.CommandLine` types can also be bound. - -## Built-in argument validation - -Arguments have expected types and [arity](syntax.md#argument-arity). `System.CommandLine` rejects arguments that don't match these expectations. - -For example, a parse error is displayed if the argument for an integer option isn't an integer. - -```console -myapp --delay not-an-int -``` - -```output -Cannot parse argument 'not-an-int' as System.Int32. -``` - -An arity error is displayed if multiple arguments are passed to an option that has maximum arity of one: - -```console -myapp --delay-option 1 --delay-option 2 -``` - -```output -Option '--delay' expects a single argument but 2 were provided. -``` - -This behavior can be overridden by setting to `true`. In that case you can repeat an option that has maximum arity of one, but only the last value on the line is accepted. In the following example, the value `three` would be passed to the app. - -```console -myapp --item one --item two --item three -``` - -## Parameter binding up to 8 options and arguments - -The following example shows how to bind options to command handler parameters, by calling : - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="intandstring" highlight="10-15" ::: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="intandstringhandler" ::: - -The lambda parameters are variables that represent the values of options and arguments: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="lambda" ::: - -The variables that follow the lambda represent the option and argument objects that are the sources of the option and argument values: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="services" ::: - - The options and arguments must be declared in the same order in the lambda and in the parameters that follow the lambda. If the order is not consistent, one of the following scenarios will result: - -* If the out-of-order options or arguments are of different types, a run-time exception is thrown. For example, an `int` might appear where a `string` should be in the list of sources. -* If the out-of-order options or arguments are of the same type, the handler silently gets the wrong values in the parameters provided to it. For example, `string` option `x` might appear where `string` option `y` should be in the list of sources. In that case, the variable for the option `y` value gets the option `x` value. - -There are overloads of that support up to 8 parameters, with both synchronous and asynchronous signatures. - -## Parameter binding more than 8 options and arguments - -To handle more than 8 options, or to construct a custom type from multiple options, you can use `InvocationContext` or a custom binder. - -### Use `InvocationContext` - -A overload provides access to the object, and you can use `InvocationContext` to get any number of option and argument values. For examples, see [Set exit codes](#set-exit-codes) and [Handle termination](handle-termination.md). - -### Use a custom binder - -A custom binder lets you combine multiple option or argument values into a complex type and pass that into a single handler parameter. Suppose you have a `Person` type: - -:::code language="csharp" source="snippets/model-binding/csharp/ComplexType.cs" id="persontype" ::: - -Create a class derived from , where `T` is the type to construct based on command line input: - -:::code language="csharp" source="snippets/model-binding/csharp/ComplexType.cs" id="personbinder" ::: - -With the custom binder, you can get your custom type passed to your handler the same way you get values for options and arguments: - -:::code language="csharp" source="snippets/model-binding/csharp/ComplexType.cs" id="sethandler" ::: - -Here's the complete program that the preceding examples are taken from: - -:::code language="csharp" source="snippets/model-binding/csharp/ComplexType.cs" id="all" ::: - -## Set exit codes - -There are -returning [Func](xref:System.Func%601) overloads of . If your handler is called from async code, you can return a [`Task`](xref:System.Threading.Tasks.Task%601) from a handler that uses one of these, and use the `int` value to set the process exit code, as in the following example: - -:::code language="csharp" source="snippets/model-binding/csharp/ReturnExitCode.cs" id="returnexitcode" ::: - -However, if the lambda itself needs to be async, you can't return a `Task`. In that case, use . You can get the `InvocationContext` instance injected into your lambda by using a SetHandler overload that specifies the `InvocationContext` as the sole parameter. This `SetHandler` overload doesn't let you specify `IValueDescriptor` objects, but you can get option and argument values from the [ParseResult](#parseresult) property of `InvocationContext`, as shown in the following example: - -:::code language="csharp" source="snippets/model-binding/csharp/ContextExitCode.cs" id="contextexitcode" ::: - -If you don't have asynchronous work to do, you can use the overloads. In that case, set the exit code by using `InvocationContext.ExitCode` the same way you would with an async lambda. - -The exit code defaults to 1. If you don't set it explicitly, its value is set to 0 when your handler exits normally. If an exception is thrown, it keeps the default value. - -## Supported types - -The following examples show code that binds some commonly used types. - -### Enums - -The values of `enum` types are bound by name, and the binding is case insensitive: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="enum" ::: - -Here's sample command-line input and resulting output from the preceding example: - -```console -myapp --color red -myapp --color RED -``` - -```output -Red -Red -``` - -### Arrays and lists - -Many common types that implement are supported. For example: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="ienumerable" ::: - -Here's sample command-line input and resulting output from the preceding example: - -```console ---items one --items two --items three -``` - -```output -System.Collections.Generic.List`1[System.String] -one -two -three -``` - -Because is set to `true`, the following input results in the same output: - -```console ---items one two three -``` - -### File system types - -Command-line applications that work with the file system can use the , , and types. The following example shows the use of `FileSystemInfo`: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="filesysteminfo" ::: - -With `FileInfo` and `DirectoryInfo` the pattern matching code is not required: - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="fileinfo" ::: - -### Other supported types - -Many types that have a constructor that takes a single string parameter can be bound in this way. For example, code that would work with `FileInfo` works with a instead. - -:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="uri" ::: - -Besides the file system types and `Uri`, the following types are supported: - -* `bool` -* `byte` -* `DateTime` -* `DateTimeOffset` -* `decimal` -* `double` -* `float` -* `Guid` -* `int` -* `long` -* `sbyte` -* `short` -* `uint` -* `ulong` -* `ushort` - -## Use System.CommandLine objects - -There's a `SetHandler` overload that gives you access to the object. That object can then be used to access other `System.CommandLine` objects. For example, you have access to the following objects: - -* -* -* -* - -### `InvocationContext` - -For examples, see [Set exit codes](#set-exit-codes) and [Handle termination](handle-termination.md). - -### `CancellationToken` - -For information about how to use , see [How to handle termination](handle-termination.md). - -### `IConsole` - - makes testing as well as many extensibility scenarios easier than using `System.Console`. It's available in the property. - -### `ParseResult` - -The object is available in the property. It's a singleton structure that represents the results of parsing the command line input. You can use it to check for the presence of options or arguments on the command line or to get the property. This property contains a list of the [tokens](syntax.md#tokens) that were parsed but didn't match any configured command, option, or argument. - -The list of unmatched tokens is useful in commands that behave like wrappers. A wrapper command takes a set of [tokens](syntax.md#tokens) and forwards them to another command or app. The `sudo` command in Linux is an example. It takes the name of a user to impersonate followed by a command to run. For example: - -```console -sudo -u admin apt update -``` - -This command line would run the `apt update` command as the user `admin`. - -To implement a wrapper command like this one, set the command property to `false`. Then the `ParseResult.UnmatchedTokens` property will contain all of the arguments that don't explicitly belong to the command. In the preceding example, `ParseResult.UnmatchedTokens` would contain the `apt` and `update` tokens. Your command handler could then forward the `UnmatchedTokens` to a new shell invocation, for example. - -## Custom validation and binding - -To provide custom validation code, call on your command, option, or argument, as shown in the following example: - -:::code language="csharp" source="snippets/model-binding/csharp/AddValidator.cs" id="delayOption" ::: - -If you want to parse as well as validate the input, use a delegate, as shown in the following example: - -:::code language="csharp" source="snippets/model-binding/csharp/ParseArgument.cs" id="delayOption" ::: - -The preceding code sets `isDefault` to `true` so that the `parseArgument` delegate will be called even if the user didn't enter a value for this option. - -Here are some examples of what you can do with `ParseArgument` that you can't do with `AddValidator`: - -* Parsing of custom types, such as the `Person` class in the following example: - - :::code language="csharp" source="snippets/model-binding/csharp/ParseArgument.cs" id="persontype" ::: - - :::code language="csharp" source="snippets/model-binding/csharp/ParseArgument.cs" id="personoption" ::: - -* Parsing of other kinds of input strings (for example, parse "1,2,3" into `int[]`). - -* Dynamic arity. For example, you have two arguments that are defined as string arrays, and you have to handle a sequence of strings in the command line input. The method enables you to dynamically divide up the input strings between the arguments. - -## See also - -[System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/parse-and-invoke.md b/docs/standard/commandline/parse-and-invoke.md new file mode 100644 index 0000000000000..8828910fca9f2 --- /dev/null +++ b/docs/standard/commandline/parse-and-invoke.md @@ -0,0 +1,123 @@ +--- +title: "How to parse and invoke the result" +description: "Learn how to get parsed values and define actions for your commands." +ms.date: 16/06/2025 +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" +--- + +# Parsing and invocation in System.CommandLine + +[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] + +System.CommandLine provides a clear separation between command-line parsing and action invocation. The parsing process is responsible for parsing command-line input and creating a object that contains the parsed values (and parse errors). The action invocation process is responsible for invoking the action associated with the parsed command, option, or directive (arguments can't have actions). + +In the following example from our [Get started with System.CommandLine](get-started-tutorial.md) tutorial, the `ParseResult` is created by parsing the command-line input. No actions are defined or invoked: + +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage0/Program.cs" id="all" ::: + +An action is invoked when a given command (or directive, or option) is parsed successfully. The action is a delegate that takes a parameter and returns an `int` exit code (async actions are also [available](#asynchronous-actions))). The exit code is returned by the method and can be used to indicate whether the command was executed successfully or not. + +In the following example from our [Get started with System.CommandLine](get-started-tutorial.md) tutorial, the action is defined for the root command and invoked after parsing the command-line input: + +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="all" ::: + +Some built-in symbols, such as , , or , come with predefined actions. These symbols are automatically added to the root command when you create it, and when you invoke the , they "just work." Using actions allows you to focus on your app logic, while the library takes care of parsing and invoking actions for built-in symbols. If you prefer, you can stick to the parsing process and not define any actions (as in the first example above). + +## ParseResult + +The type is a class that represents the results of parsing the command-line input. You need to use it to get the parsed values for options and arguments (no matter if you are using actions or not). You can also check if there were any parse errors or unmatched [tokens](syntax.md#tokens). + +### GetValue + +The method allows you to retrieve the values of options and arguments: + +:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="getvalue" ::: + +You can also get values by name, but this requires you to specify the type of the value you want to get. + +The following example uses C# collection initializers to create a root command: + +:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="collectioninitializersyntax" ::: + +Then it uses the `GetValue` method to get the values by name: + +:::code language="csharp" source="snippets/model-binding/csharp/Program.cs" id="lambdanames" ::: + +This overload of `GetValue` gets the parsed or default value for the specified symbol name, in the context of the parsed command (not the entire symbol tree). It accepts the symbol name, not an [alias](syntax.md#aliases). + +### Parse errors + +The property contains a list of parse errors that occurred during the parsing process. Each error is represented by a object, which contains information about the error, such as the error message and the token that caused the error. + +When you call the method, it returns an exit code that indicates whether the parsing was successful or not. If there were any parse errors, the exit code is non-zero, and all the parse errors are printed to the standard error. + +If you are not invoking the method, you need to handle the errors on your own, for example, by printing them: + +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage0/Program.cs" id="errors" ::: + +### Unmatched tokens + +The property contains a list of the tokens that were parsed but didn't match any configured command, option, or argument. + +The list of unmatched tokens is useful in commands that behave like wrappers. A wrapper command takes a set of [tokens](syntax.md#tokens) and forwards them to another command or app. The `sudo` command in Linux is an example. It takes the name of a user to impersonate followed by a command to run. For example: + +```console +sudo -u admin apt update +``` + +This command line would run the `apt update` command as the user `admin`. + +To implement a wrapper command like this one, set the command property to `false`. Then the property will contain all of the arguments that don't explicitly belong to the command. In the preceding example, `ParseResult.UnmatchedTokens` would contain the `apt` and `update` tokens. + +## Actions + +Actions are delegates that are invoked when a command (or an option or a directive) is parsed successfully. They take a parameter and return an `int` (or `Task`) exit code. The exit code is used to indicate whether the action was executed successfully or not. + +System.CommandLine provides an abstract base class and two derived classes: and . The former is used for synchronous actions that return an `int` exit code, while the latter is used for asynchronous actions that return a `Task` exit code. + +You don't need to create a derived type to define an action. You can use the method to set an action for a command. The synchronous action can be a delegate that takes a parameter and returns an `int` exit code. The asynchronous action can be a delegate that takes a and parameters and returns a `Task`. + +:::code language="csharp" source="snippets/get-started-tutorial/csharp/Stage1/Program.cs" id="setaction" ::: + +### Asynchronous actions + +Synchronous and asynchronous actions should not be mixed in the same application. If you want to use asynchronous actions, your application needs to be asynchronous from the top to the bottom. This means that all actions should be asynchronous, and you should use the method that accepts a delegate returning a `Task` exit code. Moreover, the that is passed to the action delegate needs to be passed further to all the methods that can be canceled, such as file I/O operations or network requests. + +On top of that, you need to ensure that the method is used instead of . This method is asynchronous and returns a `Task` exit code. It also accepts an optional parameter that can be used to cancel the action. + +The preceding code uses a `SetAction` overload that gets a [ParseResult](#parseresult) and a rather than just `ParseResult`: + +:::code language="csharp" source="snippets/handle-termination/csharp/Program.cs" id="asyncaction" ::: + +#### Process Termination Timeout + + enables signaling and handling of process termination (Ctrl+C, `SIGINT`, `SIGTERM`) via a that is passed to every async action during invocation. It's enabled by default (2 seconds), but you can set it to `null` to disable it. + +When enabled, if the action doesn't complete within the specified timeout, the process will be terminated. This is useful for handling the termination gracefully, for example, by saving the state before the process is terminated. + +To test the sample code from previous paragraph, run the command with a URL that will take a moment to load, and before it finishes loading, press Ctrl+C. On macOS press Command+Period(.). For example: + +```dotnetcli +testapp --url https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis +``` + +```output +The operation was aborted +``` + +### Exit codes + +The exit code is an integer value returned by an action indicating its success or failure. By convention, an exit code of `0` signifies success, while any non-zero value indicates an error. Its important to define meaningful exit codes in your application to communicate the status of command execution clearly. + +Every `SetAction` method has an overload that accepts a delegate returning an `int` exit code where the exit code needs to be provided in explicit way and an overload that returns `0`. + +:::code language="csharp" source="snippets/model-binding/csharp/ReturnExitCode.cs" id="returnexitcode" ::: + +## See also + +[How to customize parsing and validation in System.CommandLine](parsing-and-validation.md) +[System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/parsing-and-validation.md b/docs/standard/commandline/parsing-and-validation.md new file mode 100644 index 0000000000000..cda920e0244eb --- /dev/null +++ b/docs/standard/commandline/parsing-and-validation.md @@ -0,0 +1,79 @@ +--- +title: How to customize parsing and validation in System.CommandLine +ms.date: 06/16/2025 +description: "Learn how to customize parsing and validation with the System.CommandLine library." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" +ms.topic: how-to +--- + +# How to customize parsing and validation in System.CommandLine + +[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] + +By default, System.CommandLine provides a set of built-in parsers that can parse many common types: + +* `bool` +* `byte` and `sbyte` +* `short` and `ushort` +* `int` and `uint` +* `long` and `ulong` +* `float` and `double` +* `decimal` +* `DateTime` and `DateTimeOffset` +* `DateOnly`and `TimeOnly` +* `Guid`, +* , , and +* enums +* **arrays and lists** of the above types + +Other types are not supported, but you can create custom parsers for them. You can also validate the parsed values, which is useful when you want to ensure that the input meets certain criteria. + +## Validators + +Every option, argument, and command can have one or more validators. Validators are used to ensure that the parsed value meets certain criteria. For example, you can validate that a number is positive, or that a string is not empty. You can also create complex validators that check against multiple conditions. + +Every symbol type in System.CommandLine has a `Validators` property that contains a list of validators. The validators are executed after the input is parsed, and they can report an error if the validation fails. + +To provide custom validation code, call on your option or argument (or command), as shown in the following example: + +:::code language="csharp" source="snippets/model-binding/csharp/AddValidator.cs" id="delayOption" ::: + +System.CommandLine provides a set of built-in validators that can be used to validate common types: + +- `AcceptExistingOnly` - configures given option or argument to accept only values corresponding to an existing file or directory. +- `AcceptLegalFileNamesOnly` - configures given option or argument to accept only values representing legal file names. +- `AcceptOnlyFromAmong` - configures given option or argument to accept only values from a specified set of values. + +## Custom parsers + +Custom parsers are required to parse types with no default parser, such as complex types. They can also be used to parse supported types in a different way than the built-in parsers. + +Suppose you have a `Person` type: + +:::code language="csharp" source="snippets/model-binding/csharp/ComplexType.cs" id="persontype" ::: + +You can read the values and create an instance of `Person` in the command action: + +:::code language="csharp" source="snippets/model-binding/csharp/ComplexType.cs" id="setaction" ::: + +With a custom parser, you can get a custom type the same way you get primitive values: + +:::code language="csharp" source="snippets/model-binding/csharp/ParseArgument.cs" id="personoption" ::: + +If you want to parse as well as validate the input, use the `CustomParser` delegate, as shown in the following example: + +:::code language="csharp" source="snippets/model-binding/csharp/ParseArgument.cs" id="delayOption" ::: + +Here are some examples of what you can do with `CustomParser` that you can't do with a validator: + +* Parse other kinds of input strings (for example, parse "1,2,3" into `int[]`). +* Dynamic arity. For example, if you have two arguments that are defined as string arrays, and you have to handle a sequence of strings in the command-line input, the method enables you to dynamically divide up the input strings between the arguments. + +## See also + +- [How to parse and invoke the result](parse-and-invoke.md) +- [System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/snippets/configuration/csharp/Program.cs b/docs/standard/commandline/snippets/configuration/csharp/Program.cs new file mode 100644 index 0000000000000..ba0bc8947fc7e --- /dev/null +++ b/docs/standard/commandline/snippets/configuration/csharp/Program.cs @@ -0,0 +1,40 @@ +// +using System.CommandLine; +using System.Diagnostics; + +class Program +{ + static void Main(string[] args) + { + // + Option fileOption = new("--file") + { + Description = "An option whose argument is parsed as a FileInfo" + }; + + RootCommand rootCommand = new("Configuration sample") + { + fileOption + }; + + rootCommand.SetAction((parseResult) => + { + FileInfo? fileOptionValue = parseResult.GetValue(fileOption); + parseResult.Configuration.Output.WriteLine($"File option value: {fileOptionValue?.FullName}"); + }); + // + + // + StringWriter output = new(); + CommandLineConfiguration configuration = new(rootCommand) + { + Output = output, + Error = TextWriter.Null + }; + + configuration.Parse("-h").Invoke(); + Debug.Assert(output.ToString().Contains("Configuration sample")); + // + } +} +// diff --git a/docs/standard/commandline/snippets/dependency-injection/csharp/Properties/launchSettings.json b/docs/standard/commandline/snippets/configuration/csharp/Properties/launchSettings.json similarity index 100% rename from docs/standard/commandline/snippets/dependency-injection/csharp/Properties/launchSettings.json rename to docs/standard/commandline/snippets/configuration/csharp/Properties/launchSettings.json diff --git a/docs/standard/commandline/snippets/use-middleware/csharp/scl.csproj b/docs/standard/commandline/snippets/configuration/csharp/scl.csproj similarity index 94% rename from docs/standard/commandline/snippets/use-middleware/csharp/scl.csproj rename to docs/standard/commandline/snippets/configuration/csharp/scl.csproj index aa679cacbe895..12f4df3ddc182 100644 --- a/docs/standard/commandline/snippets/use-middleware/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/configuration/csharp/scl.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/standard/commandline/snippets/customize-help/csharp/Program.cs b/docs/standard/commandline/snippets/customize-help/csharp/Program.cs index 1e460cb8b5d65..206364be04ae6 100644 --- a/docs/standard/commandline/snippets/customize-help/csharp/Program.cs +++ b/docs/standard/commandline/snippets/customize-help/csharp/Program.cs @@ -1,178 +1,157 @@ // using System.CommandLine; -using System.CommandLine.Builder; using System.CommandLine.Help; using System.CommandLine.Invocation; -using System.CommandLine.Parsing; using Spectre.Console; class Program { - static async Task Main(string[] args) + static void Main(string[] args) { - await Original(args); - await First2Columns(args); - await DescriptionSection(args); + Original(args); + AllowedValues(args); + Sections(args); } - static async Task Original(string[] args) + static void Original(string[] args) { // - var fileOption = new Option( - "--file", - description: "The file to print out.", - getDefaultValue: () => new FileInfo("scl.runtimeconfig.json")); - var lightModeOption = new Option ( - "--light-mode", - description: "Determines whether the background color will be black or white"); - var foregroundColorOption = new Option( - "--color", - description: "Specifies the foreground color of console output", - getDefaultValue: () => ConsoleColor.White); - - var rootCommand = new RootCommand("Read a file") + Option fileOption = new("--file") { - fileOption, - lightModeOption, - foregroundColorOption + Description = "The file to print out.", + }; + Option lightModeOption = new("--light-mode") + { + Description = "Determines whether the background color will be black or white" + }; + Option foregroundColorOption = new("--color") + { + Description = "Specifies the foreground color of console output", + DefaultValueFactory = _ => ConsoleColor.White }; - rootCommand.SetHandler((file, lightMode, color) => - { - Console.BackgroundColor = lightMode ? ConsoleColor.White: ConsoleColor.Black; - Console.ForegroundColor = color; - Console.WriteLine($"--file = {file?.FullName}"); - Console.WriteLine($"File contents:\n{file?.OpenText().ReadToEnd()}"); - }, + RootCommand rootCommand = new("Read a file") + { fileOption, lightModeOption, - foregroundColorOption); - - await rootCommand.InvokeAsync(args); + foregroundColorOption + }; + rootCommand.Parse("-h").Invoke(); // - Console.WriteLine("Default help"); - await rootCommand.InvokeAsync("-h"); } - static async Task First2Columns(string[] args) + static void AllowedValues(string[] args) { - var fileOption = new Option( - "--file", - description: "The file to print out.", - getDefaultValue: () => new FileInfo("scl.runtimeconfig.json")); - var lightModeOption = new Option( - "--light-mode", - description: "Determines whether the background color will be black or white", - getDefaultValue: () => true); - var foregroundColorOption = new Option( - "--color", - description: "Specifies the foreground color of console output", - getDefaultValue: () => ConsoleColor.White); - - var rootCommand = new RootCommand("Read a file") + Option fileOption = new("--file") { - fileOption, - lightModeOption, - foregroundColorOption + Description = "The file to print out.", + }; + Option lightModeOption = new("--light-mode") + { + Description = "Determines whether the background color will be black or white" + }; + Option foregroundColorOption = new("--color") + { + Description = "Specifies the foreground color of console output", + DefaultValueFactory = _ => ConsoleColor.White, }; - rootCommand.SetHandler((file, lightMode, color) => - { - Console.BackgroundColor = lightMode ? ConsoleColor.Black : ConsoleColor.White; - Console.ForegroundColor = color; - Console.WriteLine($"--file = {file?.FullName}"); - Console.WriteLine($"File contents:\n{file?.OpenText().ReadToEnd()}"); - }, + // + fileOption.HelpName = "FILEPATH"; + foregroundColorOption.AcceptOnlyFromAmong( + ConsoleColor.Black.ToString(), + ConsoleColor.White.ToString(), + ConsoleColor.Red.ToString(), + ConsoleColor.Yellow.ToString() + ); + // + + RootCommand rootCommand = new("Read a file") + { fileOption, lightModeOption, - foregroundColorOption); - - // - fileOption.ArgumentHelpName = "FILEPATH"; - - var parser = new CommandLineBuilder(rootCommand) - .UseDefaults() - .UseHelp(ctx => - { - ctx.HelpBuilder.CustomizeSymbol(foregroundColorOption, - firstColumnText: "--color ", - secondColumnText: "Specifies the foreground color. " + - "Choose a color that provides enough contrast " + - "with the background color. " + - "For example, a yellow foreground can't be read " + - "against a light mode background."); - }) - .Build(); - - parser.Invoke(args); - // - Console.WriteLine("First two columns customized."); - await parser.InvokeAsync("-h"); + foregroundColorOption + }; + rootCommand.Parse("-h").Invoke(); } - static async Task DescriptionSection(string[] args) + static void Sections(string[] args) { - var fileOption = new Option( - "--file", - description: "The file to print out.", - getDefaultValue: () => new FileInfo("scl.runtimeconfig.json")); - var lightModeOption = new Option( - "--light-mode", - description: "Determines whether the background color will be black or white", - getDefaultValue: () => true); - var foregroundColorOption = new Option( - "--color", - description: "Specifies the foreground color of console output", - getDefaultValue: () => ConsoleColor.White); - - var rootCommand = new RootCommand("Read a file") + Option fileOption = new("--file") + { + Description = "The file to print out.", + }; + Option lightModeOption = new("--light-mode") + { + Description = "Determines whether the background color will be black or white" + }; + Option foregroundColorOption = new("--color") + { + Description = "Specifies the foreground color of console output", + DefaultValueFactory = _ => ConsoleColor.White, + }; + + fileOption.HelpName = "FILEPATH"; + foregroundColorOption.AcceptOnlyFromAmong( + ConsoleColor.Black.ToString(), + ConsoleColor.White.ToString(), + ConsoleColor.Red.ToString(), + ConsoleColor.Yellow.ToString() + ); + + RootCommand rootCommand = new("Read a file") { fileOption, lightModeOption, foregroundColorOption }; - rootCommand.SetHandler((file, lightMode, color) => + rootCommand.SetAction(parseResult => + { + if (parseResult.GetValue(fileOption) is FileInfo file) { - Console.BackgroundColor = lightMode ? ConsoleColor.Black : ConsoleColor.White; - Console.ForegroundColor = color; - Console.WriteLine($"--file = {file?.FullName}"); - Console.WriteLine($"File contents:\n{file?.OpenText().ReadToEnd()}"); - }, - fileOption, - lightModeOption, - foregroundColorOption); - - // - fileOption.ArgumentHelpName = "FILEPATH"; - - var parser = new CommandLineBuilder(rootCommand) - .UseDefaults() - .UseHelp(ctx => - { - ctx.HelpBuilder.CustomizeSymbol(foregroundColorOption, - firstColumnText: "--color ", - secondColumnText: "Specifies the foreground color. " + - "Choose a color that provides enough contrast " + - "with the background color. " + - "For example, a yellow foreground can't be read " + - "against a light mode background."); - ctx.HelpBuilder.CustomizeLayout( - _ => - HelpBuilder.Default - .GetLayout() - .Skip(1) // Skip the default command description section. - .Prepend( - _ => Spectre.Console.AnsiConsole.Write( - new FigletText(rootCommand.Description!)) - )); - }) - .Build(); - - await parser.InvokeAsync(args); - // - Console.WriteLine("Description section customized"); - await parser.InvokeAsync("-h"); + Console.BackgroundColor = parseResult.GetValue(lightModeOption) ? ConsoleColor.White : ConsoleColor.Black; + Console.ForegroundColor = parseResult.GetValue(foregroundColorOption); + + Console.WriteLine($"--file = {file.FullName}"); + Console.WriteLine($"File contents:\n{file.OpenText().ReadToEnd()}"); + } + }); + + // + for (int i = 0; i < rootCommand.Options.Count; i++) + { + // RootCommand has a default HelpOption, we need to update its Action. + if (rootCommand.Options[i] is HelpOption defaultHelpOption) + { + defaultHelpOption.Action = new CustomHelpAction((HelpAction)defaultHelpOption.Action!); + break; + } + } + // + + rootCommand.Parse("-h").Invoke(); + } + + // + internal class CustomHelpAction : SynchronousCommandLineAction + { + private readonly HelpAction _defaultHelp; + + public CustomHelpAction(HelpAction action) => _defaultHelp = action; + + public override int Invoke(ParseResult parseResult) + { + Spectre.Console.AnsiConsole.Write(new FigletText(parseResult.RootCommandResult.Command.Description!)); + + int result = _defaultHelp.Invoke(parseResult); + + Spectre.Console.AnsiConsole.WriteLine("Sample usage: --file input.txt"); + + return result; + + } } + // } // diff --git a/docs/standard/commandline/snippets/customize-help/csharp/scl.csproj b/docs/standard/commandline/snippets/customize-help/csharp/scl.csproj index 45cb223a813d5..08dc1fcdc2eaf 100644 --- a/docs/standard/commandline/snippets/customize-help/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/customize-help/csharp/scl.csproj @@ -11,7 +11,7 @@ - + diff --git a/docs/standard/commandline/snippets/define-commands/csharp/Program.cs b/docs/standard/commandline/snippets/define-commands/csharp/Program.cs deleted file mode 100644 index a32aff211c839..0000000000000 --- a/docs/standard/commandline/snippets/define-commands/csharp/Program.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -using System.CommandLine; - -class Program -{ - static async Task Main(string[] args) - { - var rootCommand = new RootCommand("Sample command-line app"); - - rootCommand.SetHandler(() => - { - Console.WriteLine("Hello world!"); - }); - - await rootCommand.InvokeAsync(args); - } -} -// diff --git a/docs/standard/commandline/snippets/define-commands/csharp/Program2.cs b/docs/standard/commandline/snippets/define-commands/csharp/Program2.cs deleted file mode 100644 index a4299fce16cf6..0000000000000 --- a/docs/standard/commandline/snippets/define-commands/csharp/Program2.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.CommandLine; - -class Program2 -{ - static async Task Main(string[] args) - { - await DefineCommands(args); - await DefineOptions(args); - await GlobalOption(args); - await RequiredOption(args); - await HiddenOption(args); - await FromAmong(args); - await DefineArguments(args); - } - - static async Task DefineArguments(string[] args) - { - // - var delayArgument = new Argument - (name: "delay", - description: "An argument that is parsed as an int.", - getDefaultValue: () => 42); - var messageArgument = new Argument - ("message", "An argument that is parsed as a string."); - - var rootCommand = new RootCommand(); - rootCommand.Add(delayArgument); - rootCommand.Add(messageArgument); - - rootCommand.SetHandler((delayArgumentValue, messageArgumentValue) => - { - Console.WriteLine($" argument = {delayArgumentValue}"); - Console.WriteLine($" argument = {messageArgumentValue}"); - }, - delayArgument, messageArgument); - - await rootCommand.InvokeAsync(args); - // - Console.WriteLine("Sample command line."); - await rootCommand.InvokeAsync("21 \"Hello world!\""); - } - - static async Task DefineCommands(string[] args) - { - // - var rootCommand = new RootCommand(); - var sub1Command = new Command("sub1", "First-level subcommand"); - rootCommand.Add(sub1Command); - var sub1aCommand = new Command("sub1a", "Second level subcommand"); - sub1Command.Add(sub1aCommand); - // - - sub1aCommand.SetHandler(() => - { - Console.WriteLine(sub1aCommand.Description); - }); - - await rootCommand.InvokeAsync(args); - } - - static async Task DefineOptions(string[] args) - { - // - var delayOption = new Option - (name: "--delay", - description: "An option whose argument is parsed as an int.", - getDefaultValue: () => 42); - var messageOption = new Option - ("--message", "An option whose argument is parsed as a string."); - - var rootCommand = new RootCommand(); - rootCommand.Add(delayOption); - rootCommand.Add(messageOption); - - rootCommand.SetHandler((delayOptionValue, messageOptionValue) => - { - Console.WriteLine($"--delay = {delayOptionValue}"); - Console.WriteLine($"--message = {messageOptionValue}"); - }, - delayOption, messageOption); - // - - await rootCommand.InvokeAsync(args); - Console.WriteLine("Sample command line."); - await rootCommand.InvokeAsync("--delay 21 --message \"Hello world!\""); - } - - static async Task GlobalOption(string[] args) - { - // - var delayOption = new Option - ("--delay", "An option whose argument is parsed as an int."); - var messageOption = new Option - ("--message", "An option whose argument is parsed as a string."); - - var rootCommand = new RootCommand(); - rootCommand.AddGlobalOption(delayOption); - rootCommand.Add(messageOption); - - var subCommand1 = new Command("sub1", "First level subcommand"); - rootCommand.Add(subCommand1); - - var subCommand1a = new Command("sub1a", "Second level subcommand"); - subCommand1.Add(subCommand1a); - - subCommand1a.SetHandler((delayOptionValue) => - { - Console.WriteLine($"--delay = {delayOptionValue}"); - }, - delayOption); - - await rootCommand.InvokeAsync(args); - // - - Console.WriteLine("Request help for second level subcommand."); - await rootCommand.InvokeAsync("sub1 sub1a -h"); - Console.WriteLine("Global option in second level subcommand."); - await rootCommand.InvokeAsync("sub1 sub1a --delay 42"); - } - - static async Task RequiredOption(string[] args) - { - // - var endpointOption = new Option("--endpoint") { IsRequired = true }; - var command = new RootCommand(); - command.Add(endpointOption); - - command.SetHandler((uri) => - { - Console.WriteLine(uri?.GetType()); - Console.WriteLine(uri?.ToString()); - }, - endpointOption); - - await command.InvokeAsync(args); - // - Console.WriteLine("Provide required option"); - await command.InvokeAsync("--endpoint https://contoso.com"); - } - - static async Task HiddenOption(string[] args) - { - // - var endpointOption = new Option("--endpoint") { IsHidden = true }; - var command = new RootCommand(); - command.Add(endpointOption); - - command.SetHandler((uri) => - { - Console.WriteLine(uri?.GetType()); - Console.WriteLine(uri?.ToString()); - }, - endpointOption); - - await command.InvokeAsync(args); - // - Console.WriteLine("Request help for hidden option."); - await command.InvokeAsync("-h"); - Console.WriteLine("Provide hidden option."); - await command.InvokeAsync("--endpoint https://contoso.com"); - } - - static async Task FromAmong(string[] args) - { - // - var languageOption = new Option( - "--language", - "An option that that must be one of the values of a static list.") - .FromAmong( - "csharp", - "fsharp", - "vb", - "pwsh", - "sql"); - // - - var rootCommand = new RootCommand("Static list example"); - rootCommand.Add(languageOption); - - rootCommand.SetHandler((languageOptionValue) => - { - Console.WriteLine($"--language = {languageOptionValue}"); - }, - languageOption); - - await rootCommand.InvokeAsync(args); - Console.WriteLine("Request help, provide a valid language, provide an invalid language."); - await rootCommand.InvokeAsync("-h"); - await rootCommand.InvokeAsync("--language csharp"); - await rootCommand.InvokeAsync("--language not-a-language"); - } -} diff --git a/docs/standard/commandline/snippets/define-symbols/csharp/Program.cs b/docs/standard/commandline/snippets/define-symbols/csharp/Program.cs new file mode 100644 index 0000000000000..23da46085b39c --- /dev/null +++ b/docs/standard/commandline/snippets/define-symbols/csharp/Program.cs @@ -0,0 +1,112 @@ +using System.CommandLine; +using System.CommandLine.Parsing; + +class Program +{ + static void Main(string[] args) + { + DefineSubcommands(args); + DefineOptions(args); + RequiredOption(args); + DefineArguments(args); + ParseErrors(args); + DefineAliases(args); + } + + static void DefineSubcommands(string[] args) + { + // + RootCommand rootCommand = new(); + + Command sub1Command = new("sub1", "First-level subcommand"); + rootCommand.Subcommands.Add(sub1Command); + + Command sub1aCommand = new("sub1a", "Second level subcommand"); + sub1Command.Subcommands.Add(sub1aCommand); + // + + // alternative syntax (not shown it the docs for now) + rootCommand = new() + { + new Command("sub1", "First-level subcommand") + { + new Command("sub1a", "Second level subcommand") + } + }; + } + + static void DefineOptions(string[] args) + { + // + Option delayOption = new("--delay", "-d") + { + Description = "An option whose argument is parsed as an int.", + DefaultValueFactory = parseResult => 42, + }; + Option messageOption = new("--message", "-m") + { + Description = "An option whose argument is parsed as a string." + }; + + RootCommand rootCommand = new(); + rootCommand.Options.Add(delayOption); + rootCommand.Options.Add(messageOption); + // + } + + static void RequiredOption(string[] args) + { + // + Option fileOption = new("--output") + { + Required = true + }; + // + } + + static void DefineArguments(string[] args) + { + // + Argument delayArgument = new("delay") + { + Description = "An argument that is parsed as an int.", + DefaultValueFactory = parseResult => 42 + }; + Argument messageArgument = new("message") + { + Description = "An argument that is parsed as a string." + }; + + RootCommand rootCommand = new(); + rootCommand.Arguments.Add(delayArgument); + rootCommand.Arguments.Add(messageArgument); + // + } + + static void ParseErrors(string[] args) + { + // + Option verbosityOption = new("--verbosity", "-v") + { + Description = "Set the verbosity level.", + }; + verbosityOption.AcceptOnlyFromAmong("quiet", "minimal", "normal", "detailed", "diagnostic"); + RootCommand rootCommand = new() { verbosityOption }; + + ParseResult parseResult = rootCommand.Parse(args); + foreach (ParseError parseError in parseResult.Errors) + { + Console.WriteLine(parseError.Message); + } + // + } + + static void DefineAliases(string[] args) + { + // + Option helpOption = new("--help", ["-h", "/h", "-?", "/?"]); + Command command = new("serialize") { helpOption }; + command.Aliases.Add("serialise"); + // + } +} diff --git a/docs/standard/commandline/snippets/define-commands/csharp/Properties/launchSettings.json b/docs/standard/commandline/snippets/define-symbols/csharp/Properties/launchSettings.json similarity index 100% rename from docs/standard/commandline/snippets/define-commands/csharp/Properties/launchSettings.json rename to docs/standard/commandline/snippets/define-symbols/csharp/Properties/launchSettings.json diff --git a/docs/standard/commandline/snippets/define-commands/csharp/scl.csproj b/docs/standard/commandline/snippets/define-symbols/csharp/scl.csproj similarity index 83% rename from docs/standard/commandline/snippets/define-commands/csharp/scl.csproj rename to docs/standard/commandline/snippets/define-symbols/csharp/scl.csproj index 853a3c4a81b2d..12f4df3ddc182 100644 --- a/docs/standard/commandline/snippets/define-commands/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/define-symbols/csharp/scl.csproj @@ -5,11 +5,10 @@ net8.0 enable enable - Program2 - + diff --git a/docs/standard/commandline/snippets/dependency-injection/csharp/Program.cs b/docs/standard/commandline/snippets/dependency-injection/csharp/Program.cs deleted file mode 100644 index 88bf68f2d87cc..0000000000000 --- a/docs/standard/commandline/snippets/dependency-injection/csharp/Program.cs +++ /dev/null @@ -1,51 +0,0 @@ -// -using System.CommandLine; -using System.CommandLine.Binding; -using Microsoft.Extensions.Logging; - -class Program -{ - static async Task Main(string[] args) - { - var fileOption = new Option( - name: "--file", - description: "An option whose argument is parsed as a FileInfo"); - - var rootCommand = new RootCommand("Dependency Injection sample"); - rootCommand.Add(fileOption); - - // - rootCommand.SetHandler(async (fileOptionValue, logger) => - { - await DoRootCommand(fileOptionValue!, logger); - }, - fileOption, new MyCustomBinder()); - // - - await rootCommand.InvokeAsync("--file scl.runtimeconfig.json"); - } - - public static async Task DoRootCommand(FileInfo aFile, ILogger logger) - { - Console.WriteLine($"File = {aFile?.FullName}"); - logger.LogCritical("Test message"); - await Task.Delay(1000); - } - - // - public class MyCustomBinder : BinderBase - { - protected override ILogger GetBoundValue( - BindingContext bindingContext) => GetLogger(bindingContext); - - ILogger GetLogger(BindingContext bindingContext) - { - using ILoggerFactory loggerFactory = LoggerFactory.Create( - builder => builder.AddConsole()); - ILogger logger = loggerFactory.CreateLogger("LoggerCategory"); - return logger; - } - } - // -} -// diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/Program.cs b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/Program.cs new file mode 100644 index 0000000000000..e86720a7c663a --- /dev/null +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/Program.cs @@ -0,0 +1,48 @@ +// +using System.CommandLine; +using System.CommandLine.Parsing; + +namespace scl; + +class Program +{ + static int Main(string[] args) + { + // + Option fileOption = new("--file") + { + Description = "The file to read and display on the console." + }; + + RootCommand rootCommand = new("Sample app for System.CommandLine"); + rootCommand.Options.Add(fileOption); + // + + // + ParseResult parseResult = rootCommand.Parse(args); + if (parseResult.GetValue(fileOption) is FileInfo parsedFile) + { + ReadFile(parsedFile); + return 0; + } + // + // + foreach (ParseError parseError in parseResult.Errors) + { + Console.Error.WriteLine(parseError.Message); + } + return 1; + // + } + + // + static void ReadFile(FileInfo file) + { + foreach (string line in File.ReadLines(file.FullName)) + { + Console.WriteLine(line); + } + } + // +} +// diff --git a/docs/standard/commandline/snippets/use-middleware/csharp/Properties/launchSettings.json b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/Properties/launchSettings.json similarity index 56% rename from docs/standard/commandline/snippets/use-middleware/csharp/Properties/launchSettings.json rename to docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/Properties/launchSettings.json index 02d8a957614dd..1e297fb50d0a8 100644 --- a/docs/standard/commandline/snippets/use-middleware/csharp/Properties/launchSettings.json +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "scl": { "commandName": "Project", - "commandLineArgs": "" + "commandLineArgs": "--file scl.runtimeconfig.json" } } } \ No newline at end of file diff --git a/docs/standard/commandline/snippets/dependency-injection/csharp/scl.csproj b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/scl.csproj similarity index 63% rename from docs/standard/commandline/snippets/dependency-injection/csharp/scl.csproj rename to docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/scl.csproj index cea74091d8df6..12f4df3ddc182 100644 --- a/docs/standard/commandline/snippets/dependency-injection/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage0/scl.csproj @@ -8,9 +8,7 @@ - - - + diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/Program.cs b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/Program.cs index 10128ed253e7c..adb7650e16fc4 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/Program.cs +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/Program.cs @@ -5,34 +5,41 @@ namespace scl; class Program { - static async Task Main(string[] args) + static int Main(string[] args) { // - // - rootCommand.SetHandler((file) => - { - ReadFile(file!); - }, - fileOption); - // + // + rootCommand.SetAction(parseResult => + { + FileInfo parsedFile = parseResult.GetValue(fileOption); + ReadFile(parsedFile); + return 0; + }); + // - return await rootCommand.InvokeAsync(args); + // + ParseResult parseResult = rootCommand.Parse(args); + return parseResult.Invoke(); + // } - // + // static void ReadFile(FileInfo file) { - File.ReadLines(file.FullName).ToList() - .ForEach(line => Console.WriteLine(line)); + foreach (string line in File.ReadLines(file.FullName)) + { + Console.WriteLine(line); + } } - // + // } // diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/scl.csproj b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/scl.csproj index afb49ace30295..68ac861bee395 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/scl.csproj +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage1/scl.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/Program.cs b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/Program.cs index d13192a42337a..7cbb46df746d8 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/Program.cs +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/Program.cs @@ -5,68 +5,67 @@ namespace scl; class Program { - static async Task Main(string[] args) + static int Main(string[] args) { - var fileOption = new Option( - name: "--file", - description: "The file to read and display on the console."); + Option fileOption = new("--file") + { + Description = "The file to read and display on the console." + }; // - var delayOption = new Option( - name: "--delay", - description: "Delay between lines, specified as milliseconds per character in a line.", - getDefaultValue: () => 42); - - var fgcolorOption = new Option( - name: "--fgcolor", - description: "Foreground color of text displayed on the console.", - getDefaultValue: () => ConsoleColor.White); - - var lightModeOption = new Option( - name: "--light-mode", - description: "Background color of text displayed on the console: default is black, light mode is white."); + Option delayOption = new("--delay") + { + Description = "Delay between lines, specified as milliseconds per character in a line.", + DefaultValueFactory = parseResult => 42 + }; + Option fgcolorOption = new("--fgcolor") + { + Description = "Foreground color of text displayed on the console.", + DefaultValueFactory = parseResult => ConsoleColor.White + }; + Option lightModeOption = new("--light-mode") + { + Description = "Background color of text displayed on the console: default is black, light mode is white." + }; // // - var rootCommand = new RootCommand("Sample app for System.CommandLine"); - //rootCommand.AddOption(fileOption); + RootCommand rootCommand = new("Sample app for System.CommandLine"); // // - var readCommand = new Command("read", "Read and display the file.") - { - fileOption, - delayOption, - fgcolorOption, - lightModeOption - }; - rootCommand.AddCommand(readCommand); + Command readCommand = new("read", "Read and display the file.") + { + fileOption, + delayOption, + fgcolorOption, + lightModeOption + }; + rootCommand.Subcommands.Add(readCommand); // - // - readCommand.SetHandler(async (file, delay, fgcolor, lightMode) => - { - await ReadFile(file!, delay, fgcolor, lightMode); - }, - fileOption, delayOption, fgcolorOption, lightModeOption); - // + // + readCommand.SetAction(parseResult => ReadFile( + parseResult.GetValue(fileOption), + parseResult.GetValue(delayOption), + parseResult.GetValue(fgcolorOption), + parseResult.GetValue(lightModeOption))); + // - return rootCommand.InvokeAsync(args).Result; + return rootCommand.Parse(args).Invoke(); } - // - internal static async Task ReadFile( - FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) + // + internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; - List lines = File.ReadLines(file.FullName).ToList(); - foreach (string line in lines) + foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); - await Task.Delay(delay * line.Length); - }; + Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); + } } - // + // } // diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/scl.csproj b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/scl.csproj index 0cd0ac8763bf4..aca260c9f5aa4 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/scl.csproj +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage2/scl.csproj @@ -14,7 +14,7 @@ - + diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs index 3623ca0af4141..e5380c4947fb3 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs @@ -5,140 +5,140 @@ namespace scl; class Program { - static async Task Main(string[] args) + static int Main(string[] args) { // - var fileOption = new Option( - name: "--file", - description: "An option whose argument is parsed as a FileInfo", - isDefault: true, - parseArgument: result => + Option fileOption = new("--file") + { + Description = "An option whose argument is parsed as a FileInfo", + Required = true, + DefaultValueFactory = result => { if (result.Tokens.Count == 0) { return new FileInfo("sampleQuotes.txt"); } - string? filePath = result.Tokens.Single().Value; + string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { - result.ErrorMessage = "File does not exist"; + result.AddError("File does not exist"); return null; } else { return new FileInfo(filePath); } - }); + } + }; // - var delayOption = new Option( - name: "--delay", - description: "Delay between lines, specified as milliseconds per character in a line.", - getDefaultValue: () => 42); - - var fgcolorOption = new Option( - name: "--fgcolor", - description: "Foreground color of text displayed on the console.", - getDefaultValue: () => ConsoleColor.White); - - var lightModeOption = new Option( - name: "--light-mode", - description: "Background color of text displayed on the console: default is black, light mode is white."); + Option delayOption = new("--delay") + { + Description = "Delay between lines, specified as milliseconds per character in a line.", + DefaultValueFactory = parseResult => 42 + }; + Option fgcolorOption = new("--fgcolor") + { + Description = "Foreground color of text displayed on the console.", + DefaultValueFactory = parseResult => ConsoleColor.White + }; + Option lightModeOption = new("--light-mode") + { + Description = "Background color of text displayed on the console: default is black, light mode is white." + }; // - var searchTermsOption = new Option( - name: "--search-terms", - description: "Strings to search for when deleting entries.") - { IsRequired = true, AllowMultipleArgumentsPerToken = true }; - - var quoteArgument = new Argument( - name: "quote", - description: "Text of quote."); - - var bylineArgument = new Argument( - name: "byline", - description: "Byline of quote."); + Option searchTermsOption = new("--search-terms") + { + Description = "Strings to search for when deleting entries.", + Required = true, + AllowMultipleArgumentsPerToken = true + }; + Argument quoteArgument = new("quote") + { + Description = "Text of quote." + }; + Argument bylineArgument = new("byline") + { + Description = "Byline of quote." + }; // // - var rootCommand = new RootCommand("Sample app for System.CommandLine"); - rootCommand.AddGlobalOption(fileOption); - - var quotesCommand = new Command("quotes", "Work with a file that contains quotes."); - rootCommand.AddCommand(quotesCommand); + RootCommand rootCommand = new("Sample app for System.CommandLine"); + fileOption.Recursive = true; + rootCommand.Options.Add(fileOption); - var readCommand = new Command("read", "Read and display the file.") - { - delayOption, - fgcolorOption, - lightModeOption - }; - quotesCommand.AddCommand(readCommand); - - var deleteCommand = new Command("delete", "Delete lines from the file."); - deleteCommand.AddOption(searchTermsOption); - quotesCommand.AddCommand(deleteCommand); - - var addCommand = new Command("add", "Add an entry to the file."); - addCommand.AddArgument(quoteArgument); - addCommand.AddArgument(bylineArgument); - addCommand.AddAlias("insert"); - quotesCommand.AddCommand(addCommand); - // + Command quotesCommand = new("quotes", "Work with a file that contains quotes."); + rootCommand.Subcommands.Add(quotesCommand); - readCommand.SetHandler(async (file, delay, fgcolor, lightMode) => - { - await ReadFile(file!, delay, fgcolor, lightMode); - }, - fileOption, delayOption, fgcolorOption, lightModeOption); + Command readCommand = new("read", "Read and display the file.") + { + delayOption, + fgcolorOption, + lightModeOption + }; + quotesCommand.Subcommands.Add(readCommand); - // - deleteCommand.SetHandler((file, searchTerms) => - { - DeleteFromFile(file!, searchTerms); - }, - fileOption, searchTermsOption); + Command deleteCommand = new("delete", "Delete lines from the file."); + deleteCommand.Options.Add(searchTermsOption); + quotesCommand.Subcommands.Add(deleteCommand); - addCommand.SetHandler((file, quote, byline) => - { - AddToFile(file!, quote, byline); - }, - fileOption, quoteArgument, bylineArgument); - // + Command addCommand = new("add", "Add an entry to the file."); + addCommand.Arguments.Add(quoteArgument); + addCommand.Arguments.Add(bylineArgument); + addCommand.Aliases.Add("insert"); + quotesCommand.Subcommands.Add(addCommand); + // - return await rootCommand.InvokeAsync(args); + readCommand.SetAction(parseResult => ReadFile( + parseResult.GetValue(fileOption), + parseResult.GetValue(delayOption), + parseResult.GetValue(fgcolorOption), + parseResult.GetValue(lightModeOption))); + + // + deleteCommand.SetAction(parseResult => DeleteFromFile( + parseResult.GetValue(fileOption), + parseResult.GetValue(searchTermsOption))); + + addCommand.SetAction(parseResult => AddToFile( + parseResult.GetValue(fileOption), + parseResult.GetValue(quoteArgument), + parseResult.GetValue(bylineArgument)) + ); + // + + return rootCommand.Parse(args).Invoke(); } - internal static async Task ReadFile( - FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) + internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; - var lines = File.ReadLines(file.FullName).ToList(); - foreach (string line in lines) + foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); - await Task.Delay(delay * line.Length); - }; - + Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); + } } - // + // internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); - File.WriteAllLines( - file.FullName, File.ReadLines(file.FullName) - .Where(line => searchTerms.All(s => !line.Contains(s))).ToList()); + + var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); + File.WriteAllLines(file.FullName, lines); } internal static void AddToFile(FileInfo file, string quote, string byline) { Console.WriteLine("Adding to file"); - using StreamWriter? writer = file.AppendText(); + + using StreamWriter writer = file.AppendText(); writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}"); writer.WriteLine($"{Environment.NewLine}-{byline}"); - writer.Flush(); } - // + // } // diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/scl.csproj b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/scl.csproj index 1fbcb46055226..0ea8471bf4dcd 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/scl.csproj +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/scl.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/standard/commandline/snippets/handle-termination/csharp/Program.cs b/docs/standard/commandline/snippets/handle-termination/csharp/Program.cs index 547f2fba162b7..b7e22c6e02efd 100644 --- a/docs/standard/commandline/snippets/handle-termination/csharp/Program.cs +++ b/docs/standard/commandline/snippets/handle-termination/csharp/Program.cs @@ -2,44 +2,36 @@ class Program { - // - static async Task Main(string[] args) + // + static Task Main(string[] args) { - int returnCode = 0; + Option urlOption = new("--url", "A URL."); + RootCommand rootCommand = new("Handle termination example") { urlOption }; - var urlOption = new Option("--url", "A URL."); - - var rootCommand = new RootCommand("Handle termination example"); - rootCommand.Add(urlOption); - - rootCommand.SetHandler(async (context) => - { - string? urlOptionValue = context.ParseResult.GetValueForOption(urlOption); - var token = context.GetCancellationToken(); - returnCode = await DoRootCommand(urlOptionValue, token); - }); - - await rootCommand.InvokeAsync(args); + rootCommand.SetAction((ParseResult parseResult, CancellationToken cancellationToken) => + { + string? urlOptionValue = parseResult.GetValue(urlOption); + return DoRootCommand(urlOptionValue, cancellationToken); + }); - return returnCode; + return rootCommand.Parse(args).InvokeAsync(); } public static async Task DoRootCommand( string? urlOptionValue, CancellationToken cancellationToken) { + using HttpClient httpClient = new(); + try { - using (var httpClient = new HttpClient()) - { - await httpClient.GetAsync(urlOptionValue, cancellationToken); - } + await httpClient.GetAsync(urlOptionValue, cancellationToken); return 0; } catch (OperationCanceledException) { - Console.Error.WriteLine("The operation was aborted"); + await Console.Error.WriteLineAsync("The operation was aborted"); return 1; } } - // + // } diff --git a/docs/standard/commandline/snippets/handle-termination/csharp/scl.csproj b/docs/standard/commandline/snippets/handle-termination/csharp/scl.csproj index aa679cacbe895..12f4df3ddc182 100644 --- a/docs/standard/commandline/snippets/handle-termination/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/handle-termination/csharp/scl.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/standard/commandline/snippets/model-binding/csharp/AddValidator.cs b/docs/standard/commandline/snippets/model-binding/csharp/AddValidator.cs index d8c9f99a5db76..7beb126eae3c8 100644 --- a/docs/standard/commandline/snippets/model-binding/csharp/AddValidator.cs +++ b/docs/standard/commandline/snippets/model-binding/csharp/AddValidator.cs @@ -5,29 +5,26 @@ class Program { - internal static async Task Main(string[] args) + internal static void Main(string[] args) { // - var delayOption = new Option("--delay"); - delayOption.AddValidator(result => + Option delayOption = new("--delay"); + delayOption.Validators.Add(result => { - if (result.GetValueForOption(delayOption) < 1) + if (result.GetValue(delayOption) < 1) { - result.ErrorMessage = "Must be greater than 0"; + result.AddError("Must be greater than 0"); } }); // - var rootCommand = new RootCommand(); - rootCommand.Add(delayOption); - - rootCommand.SetHandler((delayOptionValue) => - { - Console.WriteLine($"--delay = {delayOptionValue}"); - }, - delayOption); + RootCommand rootCommand = new() { delayOption }; + rootCommand.SetAction((parseResult) => + { + Console.WriteLine($"--delay = {parseResult.GetValue(delayOption)}"); + }); - await rootCommand.InvokeAsync(args); + rootCommand.Parse(args).Invoke(); } } // diff --git a/docs/standard/commandline/snippets/model-binding/csharp/ComplexType.cs b/docs/standard/commandline/snippets/model-binding/csharp/ComplexType.cs index d38eb0dea4890..8aa57fdd6edbd 100644 --- a/docs/standard/commandline/snippets/model-binding/csharp/ComplexType.cs +++ b/docs/standard/commandline/snippets/model-binding/csharp/ComplexType.cs @@ -2,39 +2,45 @@ // using System.CommandLine; -using System.CommandLine.Binding; public class Program { - internal static async Task Main(string[] args) + internal static void Main(string[] args) { - var fileOption = new Option( - name: "--file", - description: "An option whose argument is parsed as a FileInfo", - getDefaultValue: () => new FileInfo("scl.runtimeconfig.json")); - - var firstNameOption = new Option( - name: "--first-name", - description: "Person.FirstName"); - - var lastNameOption = new Option( - name: "--last-name", - description: "Person.LastName"); + Option fileOption = new("--file") + { + Description = "An option whose argument is parsed as a FileInfo", + DefaultValueFactory = result => new FileInfo("scl.runtimeconfig.json"), + }; + Option firstNameOption = new("--first-name") + { + Description = "Person.FirstName" + }; + Option lastNameOption = new("--last-name") + { + Description = "Person.LastName" + }; - var rootCommand = new RootCommand(); - rootCommand.Add(fileOption); - rootCommand.Add(firstNameOption); - rootCommand.Add(lastNameOption); + RootCommand rootCommand = new() + { + fileOption, + firstNameOption, + lastNameOption + }; - // - rootCommand.SetHandler((fileOptionValue, person) => + // + rootCommand.SetAction(parseResult => + { + Person person = new() { - DoRootCommand(fileOptionValue, person); - }, - fileOption, new PersonBinder(firstNameOption, lastNameOption)); - // + FirstName = parseResult.GetValue(firstNameOption), + LastName = parseResult.GetValue(lastNameOption) + }; + DoRootCommand(parseResult.GetValue(fileOption), person); + }); + // - await rootCommand.InvokeAsync(args); + rootCommand.Parse(args).Invoke(); } public static void DoRootCommand(FileInfo? aFile, Person aPerson) @@ -50,26 +56,5 @@ public class Person public string? LastName { get; set; } } // - - // - public class PersonBinder : BinderBase - { - private readonly Option _firstNameOption; - private readonly Option _lastNameOption; - - public PersonBinder(Option firstNameOption, Option lastNameOption) - { - _firstNameOption = firstNameOption; - _lastNameOption = lastNameOption; - } - - protected override Person GetBoundValue(BindingContext bindingContext) => - new Person - { - FirstName = bindingContext.ParseResult.GetValueForOption(_firstNameOption), - LastName = bindingContext.ParseResult.GetValueForOption(_lastNameOption) - }; - } - // } // diff --git a/docs/standard/commandline/snippets/model-binding/csharp/ContextExitCode.cs b/docs/standard/commandline/snippets/model-binding/csharp/ContextExitCode.cs deleted file mode 100644 index 9a70fbc6bf27f..0000000000000 --- a/docs/standard/commandline/snippets/model-binding/csharp/ContextExitCode.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.CommandLine; -using System.CommandLine.Invocation; - -namespace ContextExitCode; - -class Program -{ - // - static async Task Main(string[] args) - { - var delayOption = new Option("--delay"); - var messageOption = new Option("--message"); - - var rootCommand = new RootCommand("Parameter binding example"); - rootCommand.Add(delayOption); - rootCommand.Add(messageOption); - - rootCommand.SetHandler(async (context) => - { - int delayOptionValue = context.ParseResult.GetValueForOption(delayOption); - string? messageOptionValue = context.ParseResult.GetValueForOption(messageOption); - - Console.WriteLine($"--delay = {delayOptionValue}"); - await Task.Delay(delayOptionValue); - Console.WriteLine($"--message = {messageOptionValue}"); - context.ExitCode = 100; - }); - - return await rootCommand.InvokeAsync(args); - } -// -} diff --git a/docs/standard/commandline/snippets/model-binding/csharp/ParseArgument.cs b/docs/standard/commandline/snippets/model-binding/csharp/ParseArgument.cs index 63feaed21f9e8..642e9aae5748c 100644 --- a/docs/standard/commandline/snippets/model-binding/csharp/ParseArgument.cs +++ b/docs/standard/commandline/snippets/model-binding/csharp/ParseArgument.cs @@ -2,75 +2,69 @@ // using System.CommandLine; -using System.Security.AccessControl; class Program { - internal static async Task Main(string[] args) + internal static void Main(string[] args) { // - var delayOption = new Option( - name: "--delay", - description: "An option whose argument is parsed as an int.", - isDefault: true, - parseArgument: result => - { - if (!result.Tokens.Any()) - { - return 42; - } + Option delayOption = new("--delay") + { + Description = "An option whose argument is parsed as an int.", + CustomParser = result => + { + if (!result.Tokens.Any()) + { + return 42; + } - if (int.TryParse(result.Tokens.Single().Value, out var delay)) - { - if (delay < 1) - { - result.ErrorMessage = "Must be greater than 0"; - } - return delay; - } - else - { - result.ErrorMessage = "Not an int."; - return 0; // Ignored. - } - }); + if (int.TryParse(result.Tokens.Single().Value, out var delay)) + { + if (delay < 1) + { + result.AddError("Must be greater than 0"); + } + return delay; + } + else + { + result.AddError("Not an int."); + return 0; // Ignored. + } + } + }; // // - var personOption = new Option( - name: "--person", - description: "An option whose argument is parsed as a Person", - parseArgument: result => - { - if (result.Tokens.Count != 2) - { - result.ErrorMessage = "--person requires two arguments"; - return null; - } - return new Person - { - FirstName = result.Tokens.First().Value, - LastName = result.Tokens.Last().Value - }; - }) + Option personOption = new("--person") { - Arity = ArgumentArity.OneOrMore, - AllowMultipleArgumentsPerToken = true + Description = "An option whose argument is parsed as a Person", + CustomParser = result => + { + if (result.Tokens.Count != 2) + { + result.AddError("--person requires two arguments"); + return null; + } + return new Person + { + FirstName = result.Tokens.First().Value, + LastName = result.Tokens.Last().Value + }; + } }; // - var rootCommand = new RootCommand(); - rootCommand.Add(delayOption); - rootCommand.Add(personOption); + RootCommand rootCommand = new() { delayOption, personOption }; - rootCommand.SetHandler((delayOptionValue, personOptionValue) => - { - Console.WriteLine($"Delay = {delayOptionValue}"); - Console.WriteLine($"Person = {personOptionValue?.FirstName} {personOptionValue?.LastName}"); - }, - delayOption, personOption); + rootCommand.SetAction((parseResult) => + { + Console.WriteLine($"Delay = {parseResult.GetValue(delayOption)}"); + Person? person = parseResult.GetValue(personOption); + Console.WriteLine($"Person = {person?.FirstName} {person?.LastName}"); + }); - await rootCommand.InvokeAsync(args); + rootCommand.Parse(args).Invoke(); } // diff --git a/docs/standard/commandline/snippets/model-binding/csharp/Program.cs b/docs/standard/commandline/snippets/model-binding/csharp/Program.cs index 8ac46f5d67715..dffc90f8856f9 100644 --- a/docs/standard/commandline/snippets/model-binding/csharp/Program.cs +++ b/docs/standard/commandline/snippets/model-binding/csharp/Program.cs @@ -2,199 +2,231 @@ class Program { - static async Task Main(string[] args) + static void Main(string[] args) { - await IntAndString(args); - await Enum(args); - await ArraysAndLists(args); - await FileSystemInfoExample(args); - await FileInfoExample(args); - await Uri(args); - await ComplexType.Program.Main(args); - await ParseArgument.Program.Main(args); - await AddValidator.Program.Main(args); - await OnlyTakeExample(args); + IntAndString(args); + IntAndStringName(args); + Enum(args); + Arrays(args); + FileSystemInfoExample(args); + FileInfoExample(args); + Uri(args); + ComplexType.Program.Main(args); + ParseArgument.Program.Main(args); + AddValidator.Program.Main(args); + OnlyTakeExample(args); } - static async Task IntAndString(string[] args) + static void IntAndString(string[] args) { // - var delayOption = new Option - ("--delay", "An option whose argument is parsed as an int."); - var messageOption = new Option - ("--message", "An option whose argument is parsed as a string."); - - var rootCommand = new RootCommand("Parameter binding example"); - rootCommand.Add(delayOption); - rootCommand.Add(messageOption); - - rootCommand.SetHandler( - // - (delayOptionValue, messageOptionValue) => + Option delayOption = new("--delay") + { + Description = "An option whose argument is parsed as an int." + }; + Option messageOption = new("--message") + { + Description = "An option whose argument is parsed as a string." + }; + + RootCommand rootCommand = new("Parameter binding example") + { + delayOption, + messageOption + }; + // + + // + rootCommand.SetAction(parseResult => + { + // + int integer = parseResult.GetValue(delayOption); + string? message = parseResult.GetValue(messageOption); + // + + DisplayIntAndString(parseResult.GetValue(delayOption), message); + }); + // + + // + ParseResult parseResult = rootCommand.Parse(args); + int exitCode = parseResult.Invoke(); + // + rootCommand.Parse("--delay 42 --message \"Hello world!\"").Invoke(); + } + + static void IntAndStringName(string[] args) + { + // + RootCommand rootCommand = new("Parameter binding example") + { + new Option("--delay") { - DisplayIntAndString(delayOptionValue, messageOptionValue); + Description = "An option whose argument is parsed as an int." }, - // - // - delayOption, messageOption); - // + new Option("--message") + { + Description = "An option whose argument is parsed as a string." + } + }; + // - await rootCommand.InvokeAsync(args); - // - await rootCommand.InvokeAsync("--delay 42 --message \"Hello world!\""); + // + rootCommand.SetAction(parseResult => + { + // + int integer = parseResult.GetValue("--delay"); + string? message = parseResult.GetValue("--message"); + // + + DisplayIntAndString(integer, message); + }); + // + + // + ParseResult parseResult = rootCommand.Parse(args); + int exitCode = parseResult.Invoke(); + // + rootCommand.Parse("--delay 42 --message \"Hello world!\"").Invoke(); } - // - public static void DisplayIntAndString(int delayOptionValue, string messageOptionValue) + // + public static void DisplayIntAndString(int delayOptionValue, string? messageOptionValue) { Console.WriteLine($"--delay = {delayOptionValue}"); Console.WriteLine($"--message = {messageOptionValue}"); } - // + // // - static async Task Enum(string[] args) + static void Enum(string[] args) { // - var colorOption = new Option("--color"); - - var rootCommand = new RootCommand("Enum binding example"); - rootCommand.Add(colorOption); + Option colorOption = new("--color"); + RootCommand rootCommand = new("Enum binding example") { colorOption }; - rootCommand.SetHandler((colorOptionValue) => - { Console.WriteLine(colorOptionValue); }, - colorOption); + rootCommand.SetAction(parseResult => Console.WriteLine(parseResult.GetValue(colorOption))); - await rootCommand.InvokeAsync(args); + rootCommand.Parse(args).Invoke(); // - await rootCommand.InvokeAsync("--color red"); - await rootCommand.InvokeAsync("--color RED"); + rootCommand.Parse("--color red").Invoke(); + rootCommand.Parse("--color RED").Invoke(); } - static async Task ArraysAndLists(string[] args) + + static void Arrays(string[] args) { - // - var itemsOption = new Option>("--items") - { AllowMultipleArgumentsPerToken = true }; + // + Option itemsOption = new("--items") + { + AllowMultipleArgumentsPerToken = true + }; - var command = new RootCommand("IEnumerable binding example"); - command.Add(itemsOption); + RootCommand command = new("Array binding example") { itemsOption }; - command.SetHandler((items) => + command.SetAction(parseResult => + { + foreach (string item in parseResult.GetValue(itemsOption)) { - Console.WriteLine(items.GetType()); - - foreach (string item in items) - { - Console.WriteLine(item); - } - }, - itemsOption); + Console.WriteLine(item); + } + }); - await command.InvokeAsync(args); - // - await command.InvokeAsync("--items one --items two --items three"); - await command.InvokeAsync("--items one two three"); + command.Parse(args).Invoke(); + // + command.Parse("--items one --items two --items three").Invoke(); + command.Parse("--items one two three").Invoke(); } - static async Task FileSystemInfoExample(string[] args) + + static void FileSystemInfoExample(string[] args) { // - var fileOrDirectoryOption = new Option("--file-or-directory"); - - var command = new RootCommand(); - command.Add(fileOrDirectoryOption); + Option fileOrDirectoryOption = new("--file-or-directory"); + RootCommand command = new() { fileOrDirectoryOption }; - command.SetHandler((fileSystemInfo) => + command.SetAction((parseResult) => + { + switch (parseResult.GetValue(fileOrDirectoryOption)) { - switch (fileSystemInfo) - { - case FileInfo file : - Console.WriteLine($"File name: {file.FullName}"); - break; - case DirectoryInfo directory: - Console.WriteLine($"Directory name: {directory.FullName}"); - break; - default: - Console.WriteLine("Not a valid file or directory name."); - break; - } - }, - fileOrDirectoryOption); + case FileInfo file: + Console.WriteLine($"File name: {file.FullName}"); + break; + case DirectoryInfo directory: + Console.WriteLine($"Directory name: {directory.FullName}"); + break; + default: + Console.WriteLine("Not a valid file or directory name."); + break; + } + }); - await command.InvokeAsync(args); + command.Parse(args).Invoke(); // - await command.InvokeAsync("--file-or-directory scl.runtimeconfig.json"); - await command.InvokeAsync("--file-or-directory ../net6.0"); - await command.InvokeAsync("--file-or-directory newfile.json"); + command.Parse("--file-or-directory scl.runtimeconfig.json").Invoke(); + command.Parse("--file-or-directory ../net8.0").Invoke(); + command.Parse("--file-or-directory newfile.json").Invoke(); } - static async Task FileInfoExample(string[] args) + static void FileInfoExample(string[] args) { // - var fileOption = new Option("--file"); - - var command = new RootCommand(); - command.Add(fileOption); + Option fileOption = new("--file"); + RootCommand command = new() { fileOption }; - command.SetHandler((file) => + command.SetAction((paseResult) => + { + if (paseResult.GetValue(fileOption) is FileInfo file) { - if (file is not null) - { - Console.WriteLine($"File name: {file?.FullName}"); - } - else - { - Console.WriteLine("Not a valid file name."); - } - }, - fileOption); + Console.WriteLine($"File name: {file?.FullName}"); + } + else + { + Console.WriteLine("Not a valid file name."); + } + }); - await command.InvokeAsync(args); + command.Parse(args).Invoke(); // - await command.InvokeAsync("--file scl.runtimeconfig.json"); - await command.InvokeAsync("--file newfile.json"); + command.Parse("--file scl.runtimeconfig.json").Invoke(); + command.Parse("--file newfile.json").Invoke(); } - static async Task Uri(string[] args) + static void Uri(string[] args) { // - var endpointOption = new Option("--endpoint"); - - var command = new RootCommand(); - command.Add(endpointOption); + Option endpointOption = new("--endpoint"); + RootCommand command = new() { endpointOption }; - command.SetHandler((uri) => - { - Console.WriteLine($"URL: {uri?.ToString()}"); - }, - endpointOption); + command.SetAction((parseResult) => + { + Console.WriteLine($"URL: {parseResult.GetValue(endpointOption)?.ToString()}"); + }); - await command.InvokeAsync(args); + command.Parse(args).Invoke(); // - await command.InvokeAsync("--endpoint https://contoso.com"); + command.Parse("--endpoint https://contoso.com").Invoke(); } - static async Task OnlyTakeExample(string[] args) + static void OnlyTakeExample(string[] args) { // - var arg1 = new Argument(name: "arg1", parse: result => + Argument arg1 = new("arg1") { - result.OnlyTake(2);//System.CommandLine.Parsing.ArgumentResult.OnlyTake - return result.Tokens.Select(t => t.Value).ToArray(); - }); - var arg2 = new Argument("arg2"); + CustomParser = result => + { + result.OnlyTake(2); // System.CommandLine.Parsing.ArgumentResult.OnlyTake + return result.Tokens.Select(t => t.Value).ToArray(); + } + }; + Argument arg2 = new("arg2"); - var rootCommand = new RootCommand + RootCommand rootCommand = new() { arg1, arg2 }; + rootCommand.SetAction(parseResult => { - arg1, arg2 - }; - rootCommand.SetHandler((arg1Value, arg2Value) => - { - Console.WriteLine($"arg1 = {String.Concat(arg1Value)}"); - Console.WriteLine($"arg2 = {String.Concat(arg2Value)}"); - }, - arg1, arg2); - await rootCommand.InvokeAsync(args); + Console.WriteLine($"arg1 = {String.Concat(parseResult.GetValue(arg1))}"); + Console.WriteLine($"arg2 = {String.Concat(parseResult.GetValue(arg2))}"); + }); + rootCommand.Parse(args).Invoke(); // - await rootCommand.InvokeAsync("1 2 3 4 5"); + rootCommand.Parse("1 2 3 4 5").Invoke(); } } diff --git a/docs/standard/commandline/snippets/model-binding/csharp/ReturnExitCode.cs b/docs/standard/commandline/snippets/model-binding/csharp/ReturnExitCode.cs index 375bc95806e73..f616dd41fccb3 100644 --- a/docs/standard/commandline/snippets/model-binding/csharp/ReturnExitCode.cs +++ b/docs/standard/commandline/snippets/model-binding/csharp/ReturnExitCode.cs @@ -5,24 +5,26 @@ namespace ReturnExitCode; class Program { // - static async Task Main(string[] args) + static int Main(string[] args) { - var delayOption = new Option("--delay"); - var messageOption = new Option("--message"); + Option delayOption = new("--delay"); + Option messageOption = new("--message"); - var rootCommand = new RootCommand("Parameter binding example"); - rootCommand.Add(delayOption); - rootCommand.Add(messageOption); + RootCommand rootCommand = new("Parameter binding example") + { + delayOption, + messageOption + }; - rootCommand.SetHandler((delayOptionValue, messageOptionValue) => - { - Console.WriteLine($"--delay = {delayOptionValue}"); - Console.WriteLine($"--message = {messageOptionValue}"); - return Task.FromResult(100); - }, - delayOption, messageOption); + rootCommand.SetAction(parseResult => + { + Console.WriteLine($"--delay = {parseResult.GetValue(delayOption)}"); + Console.WriteLine($"--message = {parseResult.GetValue(messageOption)}"); + // Value returned from the action delegate is the exit code. + return 100; + }); - return await rootCommand.InvokeAsync(args); + return rootCommand.Parse(args).Invoke(); } // } diff --git a/docs/standard/commandline/snippets/model-binding/csharp/scl.csproj b/docs/standard/commandline/snippets/model-binding/csharp/scl.csproj index f3a9c72dd5854..efa7172edb5e3 100644 --- a/docs/standard/commandline/snippets/model-binding/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/model-binding/csharp/scl.csproj @@ -9,7 +9,7 @@ - + diff --git a/docs/standard/commandline/snippets/tab-completion/csharp/Program.cs b/docs/standard/commandline/snippets/tab-completion/csharp/Program.cs index 1051756a3e906..b7748a7870fce 100644 --- a/docs/standard/commandline/snippets/tab-completion/csharp/Program.cs +++ b/docs/standard/commandline/snippets/tab-completion/csharp/Program.cs @@ -3,24 +3,28 @@ using System.CommandLine.Completions; using System.CommandLine.Parsing; -await new DateCommand().InvokeAsync(args); +new DateCommand().Parse(args).Invoke(); class DateCommand : Command { - private Argument subjectArgument = - new ("subject", "The subject of the appointment."); - private Option dateOption = - new ("--date", "The day of week to schedule. Should be within one week."); - + private Argument subjectArgument = new("subject") + { + Description = "The subject of the appointment." + }; + private Option dateOption = new("--date") + { + Description = "The day of week to schedule. Should be within one week." + }; + public DateCommand() : base("schedule", "Makes an appointment for sometime in the next week.") { - this.AddArgument(subjectArgument); - this.AddOption(dateOption); + this.Arguments.Add(subjectArgument); + this.Options.Add(dateOption); // - dateOption.AddCompletions((ctx) => { + dateOption.CompletionSources.Add(ctx => { var today = System.DateTime.Today; - var dates = new List(); + List dates = new(); foreach (var i in Enumerable.Range(1, 7)) { var date = today.AddDays(i); @@ -34,11 +38,10 @@ class DateCommand : Command }); // - this.SetHandler((subject, date) => - { - Console.WriteLine($"Scheduled \"{subject}\" for {date}"); - }, - subjectArgument, dateOption); + this.SetAction(parseResult => + { + Console.WriteLine($"Scheduled \"{parseResult.GetValue(subjectArgument)}\" for {parseResult.GetValue(dateOption)}"); + }); } } // diff --git a/docs/standard/commandline/snippets/tab-completion/csharp/scl.csproj b/docs/standard/commandline/snippets/tab-completion/csharp/scl.csproj index 12a922fe58ce4..c16146fe9b652 100644 --- a/docs/standard/commandline/snippets/tab-completion/csharp/scl.csproj +++ b/docs/standard/commandline/snippets/tab-completion/csharp/scl.csproj @@ -9,7 +9,7 @@ - + diff --git a/docs/standard/commandline/snippets/use-middleware/csharp/Program.cs b/docs/standard/commandline/snippets/use-middleware/csharp/Program.cs deleted file mode 100644 index 8909a31b9177b..0000000000000 --- a/docs/standard/commandline/snippets/use-middleware/csharp/Program.cs +++ /dev/null @@ -1,51 +0,0 @@ -// -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Parsing; - -class Program -{ - static async Task Main(string[] args) - { - var delayOption = new Option("--delay"); - var messageOption = new Option("--message"); - - var rootCommand = new RootCommand("Middleware example"); - rootCommand.Add(delayOption); - rootCommand.Add(messageOption); - - rootCommand.SetHandler((delayOptionValue, messageOptionValue) => - { - DoRootCommand(delayOptionValue, messageOptionValue); - }, - delayOption, messageOption); - - // - var commandLineBuilder = new CommandLineBuilder(rootCommand); - - commandLineBuilder.AddMiddleware(async (context, next) => - { - if (context.ParseResult.Directives.Contains("just-say-hi")) - { - context.Console.WriteLine("Hi!"); - } - else - { - await next(context); - } - }); - - commandLineBuilder.UseDefaults(); - var parser = commandLineBuilder.Build(); - await parser.InvokeAsync(args); - // - } - - public static void DoRootCommand(int delay, string message) - { - Console.WriteLine($"--delay = {delay}"); - Console.WriteLine($"--message = {message}"); - } -} -// - diff --git a/docs/standard/commandline/syntax.md b/docs/standard/commandline/syntax.md index 79648d416fd72..56b2bedbe7070 100644 --- a/docs/standard/commandline/syntax.md +++ b/docs/standard/commandline/syntax.md @@ -1,6 +1,6 @@ --- title: Command-line syntax overview for System.CommandLine -description: "An introduction to the command-line syntax that the System.CommandLine library recognizes by default. Mentions exceptions where syntax in the .NET CLI differs. Provides guidance for designing a command-line interface." +description: "An introduction to the command-line syntax that the System.CommandLine library recognizes by default. Shows how to define commands, options, and arguments." ms.date: 05/24/2022 no-loc: [System.CommandLine] helpviewer_keywords: @@ -10,11 +10,11 @@ helpviewer_keywords: ms.topic: conceptual --- -# Command-line syntax overview for System.CommandLine +# Syntax overview: commands, options, and arguments [!INCLUDE [scl-preview](../../../includes/scl-preview.md)] -This article explains the command-line syntax that `System.CommandLine` recognizes. The information will be useful to users as well as developers of .NET command-line apps, including the [.NET CLI](../../core/tools/index.md). +This article explains the command-line syntax that `System.CommandLine` recognizes. The information is useful to both users and developers of .NET command-line apps, including the [.NET CLI](../../core/tools/index.md). ## Tokens @@ -50,16 +50,28 @@ A *command* in command-line input is a token that specifies an action or defines * In `dotnet run`, `run` is a command that specifies an action. * In `dotnet tool install`, `install` is a command that specifies an action, and `tool` is a command that specifies a group of related commands. There are other tool-related commands, such as `tool uninstall`, `tool list`, and `tool update`. -### Root commands +### Root command The *root command* is the one that specifies the name of the app's executable. For example, the `dotnet` command specifies the *dotnet.exe* executable. + is the general-purpose class for any command or subcommand, while is a specialized version intended for the application's root entry point, inheriting all features of but adding root-specific behavior and defaults, such as [Help option](help.md#help-option), [Version option](#version-option) and [Suggest directive](#suggest-directive). + ### Subcommands Most command-line apps support *subcommands*, also known as *verbs*. For example, the `dotnet` command has a `run` subcommand that you invoke by entering `dotnet run`. Subcommands can have their own subcommands. In `dotnet tool install`, `install` is a subcommand of `tool`. +You can add subcommands as shown in the following example: + +:::code language="csharp" source="snippets/define-symbols/csharp/Program.cs" id="definesubcommands" ::: + +The innermost subcommand in this example can be invoked like this: + +```console +myapp sub1 sub1a +``` + ## Options An option is a named parameter that can be passed to a command. [POSIX](https://en.wikipedia.org/wiki/POSIX) CLIs typically prefix the option name with two hyphens (`--`). The following example shows two options: @@ -78,23 +90,38 @@ msbuild /version ^------^ ``` -`System.CommandLine` supports both POSIX and Windows prefix conventions. When you [configure an option](define-commands.md#define-options), you specify the option name including the prefix. +`System.CommandLine` supports both POSIX and Windows prefix conventions. -## Arguments +When you configure an option, you specify the option name including the prefix: -An argument is a value passed to an option or a command. The following examples show an argument for the `verbosity` option and an argument for the `build` command. +:::code language="csharp" source="snippets/define-symbols/csharp/Program.cs" id="defineoptions" ::: -```console -dotnet tool update dotnet-suggest --verbosity quiet --global - ^---^ -``` +To add an option to a command and recursively to all of its subcommands, use the property. + +### Required Options + +Some options have required arguments. For example in the .NET CLI, `--output` requires a folder name argument. If the argument is not provided, the command fails. To make an option required, set its property to `true`, as shown in the following example: + +:::code language="csharp" source="snippets/define-symbols/csharp/Program.cs" id="requiredoption" ::: + +If a required option has a default value (specified via `DefaultValueFactory` property), the option doesn't have to be specified on the command line. In that case, the default value provides the required option value. + +## Arguments + +An argument is an unnamed parameter that can be passed to a command. The following example shows an argument for the `build` command. ```console dotnet build myapp.csproj ^----------^ ``` -Arguments can have default values that apply if no argument is explicitly provided. For example, many options are implicitly Boolean parameters with a default of `true` when the option name is in the command line. The following command-line examples are equivalent: +When you configure an argument, you specify the argument name (it's not used for parsing, but it can be used for getting parsed values by name or displaying help) and type: + +:::code language="csharp" source="snippets/define-symbols/csharp/Program.cs" id="definearguments" ::: + +## Default Values + +Both arguments and options can have default values that apply if no argument is explicitly provided. For example, many options are implicitly Boolean parameters with a default of `true` when the option name is in the command line. The following command-line examples are equivalent: ```dotnetcli dotnet tool update dotnet-suggest --global @@ -104,21 +131,25 @@ dotnet tool update dotnet-suggest --global true ^-----------^ ``` -Some options have required arguments. For example in the .NET CLI, `--output` requires a folder name argument. If the argument is not provided, the command fails. +An argument that is defined without a default value is treated as a required argument. -Arguments can have expected types, and `System.CommandLine` displays an error message if an argument can't be parsed into the expected type. For example, the following command errors because "silent" isn't one of the valid values for `--verbosity`: +## Parse Errors + +Options and arguments have expected types, and an error is produced when the value can't be parsed. For example, the following command errors because "silent" isn't one of the valid values for `--verbosity`: ```dotnetcli dotnet build --verbosity silent ``` +:::code language="csharp" source="snippets/define-symbols/csharp/Program.cs" id="parseerrors" ::: + ```output -Cannot parse argument 'silent' for option '-v' as expected type 'Microsoft.DotNet.Cli.VerbosityOptions'. Did you mean one of the following? -Detailed -Diagnostic -Minimal -Normal -Quiet +Argument 'silent' not recognized. Must be one of: + 'quiet' + 'minimal' + 'normal' + 'detailed' + 'diagnostic' ``` Arguments also have expectations about how many values can be provided. Examples are provided in the [section on argument arity](#argument-arity). @@ -139,18 +170,16 @@ dotnet add package System.CommandLine --prerelease --no-restore --source https:/ dotnet add package System.CommandLine --source https://api.nuget.org/v3/index.json --no-restore --prerelease ``` -When there are multiple arguments, the order does matter. The following commands are not necessarily equivalent: +When there are multiple arguments, the order does matter. The following commands are not equivalent, they differ in the order of the values, which could lead to different results: ```console myapp argument1 argument2 myapp argument2 argument1 ``` -These commands pass a list with the same values to the command handler code, but they differ in the order of the values, which could lead to different results. - ## Aliases -In both POSIX and Windows, it's common for some commands and options to have aliases. These are usually short forms that are easier to type. Aliases can also be used for other purposes, such as to [simulate case-insensitivity](#case-sensitivity) and to [support alternate spellings of a word](define-commands.md#define-aliases). +In both POSIX and Windows, it's common for some commands and options to have aliases. These are usually short forms that are easier to type. Aliases can also be used for other purposes, such as to [simulate case-insensitivity](#case-sensitivity) and to support alternate spellings of a word. POSIX short forms typically have a single leading hyphen followed by a single character. The following commands are equivalent: @@ -170,7 +199,11 @@ dotnet publish --ou ./publish dotnet publish --o ./publish ``` -`System.CommandLine` doesn't support automatic aliases. +`System.CommandLine` doesn't support automatic aliases, each alias needs to be specified in explicit way. Both commands and options expose `Aliases` property, but `Option` has a constructor that accepts aliases as parameters, so you can define an option with multiple aliases in a single line: + +:::code language="csharp" source="snippets/define-symbols/csharp/Program.cs" id="definealiases" ::: + +We recommend that you minimize the number of option aliases that you define, and avoid defining certain aliases in particular. For more information, see [Short-form aliases](design-guidance.md#short-form-aliases). ## Case sensitivity @@ -253,7 +286,13 @@ Arity is expressed with a minimum value and a maximum value, as the following ta * - May have one value, multiple values, or no values. * - May have multiple values, must have at least one value. -Arity can often be inferred from the type. For example, an `int` option has arity of `ExactlyOne`, and a `List` option has arity `OneOrMore`. +You can explicitly set arity by using the `Arity` property, but in most cases that is not necessary. `System.CommandLine` automatically determines the argument arity based on the argument type: + +| Argument type | Default arity | +|------------------|----------------------------| +| `Boolean` | `ArgumentArity.ZeroOrOne` | +| Collection types | `ArgumentArity.ZeroOrMore` | +| Everything else | `ArgumentArity.ExactlyOne` | ### Option overrides @@ -265,12 +304,22 @@ myapp --delay 3 --message example --delay 2 ### Multiple arguments -If the arity maximum is more than one, `System.CommandLine` can be configured to accept multiple arguments for one option without repeating the option name. +By default, when you call a command, you can repeat an option name to specify multiple arguments for an option that has maximum [arity](#argument-arity) greater than one. + +```console +myapp --items one --items two --items three +``` + +To allow multiple arguments without repeating the option name, set to `true`. This setting lets you enter the following command line. + +```console +myapp --items one two three +``` -In the following example, the list passed to the `myapp` command would contain "a", "b", "c", and "d": +The same setting has a different effect if maximum argument arity is 1. It allows you to repeat an option but takes only the last value on the line. In the following example, the value `three` would be passed to the app. ```console -myapp --list a b c --list d +myapp --item one --item two --item three ``` ## Option bundling @@ -309,45 +358,7 @@ False True ``` -## The --help option - -Command-line apps typically provide an option to display a brief description of the available commands, options, and arguments. `System.CommandLine` automatically generates help output. For example: - -```dotnetcli -dotnet list --help -``` - -```output -Description: - List references or packages of a .NET project. - -Usage: - dotnet [options] list [] [command] - -Arguments: - The project or solution file to operate on. If a file is not specified, the command will search the current directory for one. - -Options: - -?, -h, --help Show command line help. - -Commands: - package List all package references of the project or solution. - reference List all project-to-project references of the project. -``` - -App users might be accustomed to different ways to request help on different platforms, so apps built on `System.CommandLine` respond to many ways of requesting help. The following commands are all equivalent: - -```dotnetcli -dotnet --help -dotnet -h -dotnet /h -dotnet -? -dotnet /? -``` - -Help output doesn't necessarily show all available commands, arguments, and options. Some of them may be *hidden*, which means they don't show up in help output but they can be specified on the command line. - -## The --version option +## Version option Apps built on `System.CommandLine` automatically provide the version number in response to the `--version` option used with the root command. For example: @@ -401,10 +412,10 @@ Here are syntax rules that determine how the text in a response file is interpre ## Directives -`System.CommandLine` introduces a syntactic element called a *directive*. The `[parse]` directive is an example. When you include `[parse]` after the app's name, `System.CommandLine` displays a diagram of the parse result instead of invoking the command-line app: +`System.CommandLine` introduces a syntactic element called a *directive* represented by type. The `[diagram]` directive is an example. When you include `[diagram]` after the app's name, `System.CommandLine` displays a diagram of the parse result instead of invoking the command-line app: ```dotnetcli -dotnet [parse] build --no-restore --output ./build-output/ +dotnet [diagram] build --no-restore --output ./build-output/ ^-----^ ``` @@ -426,15 +437,15 @@ A directive can include an argument, separated from the directive name by a colo The following directives are built in: -* [`[parse]`](#the-parse-directive) -* [`[suggest]`](#the-suggest-directive) +* [`[diagram]`](#the-diagram-directive) +* [`[suggest]`](#suggest-directive) -### The `[parse]` directive +### The `[diagram]` directive -Both users and developers may find it useful to see how an app will interpret a given input. One of the default features of a `System.CommandLine` app is the `[parse]` directive, which lets you preview the result of parsing command input. For example: +Both users and developers may find it useful to see how an app will interpret a given input. One of the default features of a `System.CommandLine` app is the `[diagram]` directive, which lets you preview the result of parsing command input. For example: ```console -myapp [parse] --delay not-an-int --interactive --file filename.txt extra +myapp [diagram] --delay not-an-int --interactive --file filename.txt extra ``` ```output @@ -448,7 +459,7 @@ In the preceding example: * For the option result `*[ --fgcolor ]`, the option wasn't specified on the command line, so the configured default was used. `White` is the effective value for this option. The asterisk indicates that the value is the default. * `???-->` points to input that wasn't matched to any of the app's commands or options. -### The `[suggest]` directive +### Suggest directive The `[suggest]` directive lets you search for commands when you don't know the exact command. @@ -462,156 +473,9 @@ build-server msbuild ``` -## Design guidance - -The following sections present guidance that we recommend you follow when designing a CLI. Think of what your app expects on the command line as similar to what a REST API server expects in the URL. Consistent rules for REST APIs are what make them readily usable to client app developers. In the same way, users of your command-line apps will have a better experience if the CLI design follows common patterns. - -Once you create a CLI it is hard to change, especially if your users have used your CLI in scripts they expect to keep running. The guidelines here were developed after the .NET CLI, and it doesn't always follow these guidelines. We are updating the .NET CLI where we can do it without introducing breaking changes. An example of this work is the new design for `dotnet new` in .NET 7. - -### Commands and subcommands - -If a command has subcommands, the command should function as an area, or a grouping identifier for the subcommands, rather than specify an action. When you invoke the app, you specify the grouping command and one of its subcommands. For example, try to run `dotnet tool`, and you get an error message because the `tool` command only identifies a group of tool-related subcommands, such as `install` and `list`. You can run `dotnet tool install`, but `dotnet tool` by itself would be incomplete. - -One of the ways that defining areas helps your users is that it organizes the help output. - -Within a CLI there is often an implicit area. For example, in the .NET CLI, the implicit area is the project and in the Docker CLI it is the image. As a result, you can use `dotnet build` without including an area. Consider whether your CLI has an implicit area. If it does, consider whether to allow the user to optionally include or omit it as in `docker build` and `docker image build`. If you optionally allow the implicit area to be typed by your user, you also automatically have help and tab completion for this grouping of commands. Supply the optional use of the implicit group by defining two commands that perform the same operation. - -### Options as parameters - -Options should provide parameters to commands, rather than specifying actions themselves. This is a recommended design principle although it isn't always followed by `System.CommandLine` (`--help` displays help information). - -### Short-form aliases - -In general, we recommend that you minimize the number of short-form option aliases that you define. - -In particular, avoid using any of the following aliases differently than their common usage in the .NET CLI and other .NET command-line apps: - -* `-i` for `--interactive`. - - This option signals to the user that they may be prompted for inputs to questions that the command needs answered. For example, prompting for a username. Your CLI may be used in scripts, so use caution in prompting users that have not specified this switch. - -* `-o` for `--output`. - - Some commands produce files as the result of their execution. This option should be used to help determine where those files should be located. In cases where a single file is created, this option should be a file path. In cases where many files are created, this option should be a directory path. - -* `-v` for `--verbosity`. - - Commands often provide output to the user on the console; this option is used to specify the amount of output the user requests. For more information, see [The `--verbosity` option](#the---verbosity-option) later in this article. - -There are also some aliases with common usage limited to the .NET CLI. You can use these aliases for other options in your apps, but be aware of the possibility of confusion. - -* `-c` for `--configuration` - - This option often refers to a named Build Configuration, like `Debug` or `Release`. You can use any name you want for a configuration, but most tools are expecting one of those. This setting is often used to configure other properties in a way that makes sense for that configuration—for example, doing less code optimization when building the `Debug` configuration. Consider this option if your command has different modes of operation. - -* `-f` for `--framework` - - This option is used to select a single [Target Framework Moniker (TFM)](../frameworks.md) to execute for, so if your CLI application has differing behavior based on which TFM is chosen, you should support this flag. - -* `-p` for `--property` - - If your application eventually invokes MSBuild, the user will often need to customize that call in some way. This option allows for MSBuild properties to be provided on the command line and passed on to the underlying MSBuild call. If your app doesn't use MSBuild but needs a set of key-value pairs, consider using this same option name to take advantage of users' expectations. - -* `-r` for `--runtime` - - If your application can run on different runtimes, or has runtime-specific logic, consider supporting this option as a way of specifying a [Runtime Identifier](../../core/rid-catalog.md). If your app supports --runtime, consider supporting `--os` and `--arch` also. These options let you specify just the OS or the architecture parts of the RID, leaving the part not specified to be determined from the current platform. For more information, see [dotnet publish](../../core/tools/dotnet-publish.md). - -### Short names - -Make names for commands, options, and arguments as short and easy to spell as possible. For example, if `class` is clear enough don't make the command `classification`. - -### Lowercase names - -Define names in lowercase only, except you can make uppercase aliases to make commands or options case insensitive. - -### Kebab case names - -Use [kebab case](https://en.wikipedia.org/wiki/Letter_case#Kebab_case) to distinguish words. For example, `--additional-probing-path`. - -### Pluralization - -Within an app, be consistent in pluralization. For example, don't mix plural and singular names for options that can have multiple values (maximum arity greater than one): - -| Option names | Consistency | -|----------------------------------------------|--------------| -| `--additional-probing-paths` and `--sources` | ✔️ | -| `--additional-probing-path` and `--source` | ✔️ | -| `--additional-probing-paths` and `--source` | ❌ | -| `--additional-probing-path` and `--sources` | ❌ | - -### Verbs vs. nouns - -Use verbs rather than nouns for commands that refer to actions (those without subcommands under them), for example: `dotnet workload remove`, not `dotnet workload removal`. And use nouns rather than verbs for options, for example: `--configuration`, not `--configure`. - -### The `--verbosity` option - -`System.CommandLine` applications typically offer a `--verbosity` option that specifies how much output is sent to the console. Here are the standard five settings: - -* `Q[uiet]` -* `M[inimal]` -* `N[ormal]` -* `D[etailed]` -* `Diag[nostic]` - -These are the standard names, but existing apps sometimes use `Silent` in place of `Quiet`, and `Trace`, `Debug`, or `Verbose` in place of `Diagnostic`. - -Each app defines its own criteria that determine what gets displayed at each level. Typically an app only needs three levels: - -* Quiet -* Normal -* Diagnostic - -If an app doesn't need five different levels, the option should still define the same five settings. In that case, `Minimal` and `Normal` will produce the same output, and `Detailed` and `Diagnostic` will likewise be the same. This allows your users to just type what they are familiar with, and the best fit will be used. - -The expectation for `Quiet` is that no output is displayed on the console. However, if an app offers an interactive mode, the app should do one of the following alternatives: - -* Display prompts for input when `--interactive` is specified, even if `--verbosity` is `Quiet`. -* Disallow the use of `--verbosity Quiet` and `--interactive` together. - -Otherwise the app will wait for input without telling the user what it's waiting for. It will appear that your application froze and the user will have no idea the application is waiting for input. - -If you define aliases, use `-v` for `--verbosity` and make `-v` without an argument an alias for `--verbosity Diagnostic`. Use `-q` for `--verbosity Quiet`. - -## The .NET CLI and POSIX conventions - -The .NET CLI does not consistently follow all POSIX conventions. - -### Double-dash - -Several commands in the .NET CLI have a special implementation of the double-dash token. In the case of `dotnet run`, `dotnet watch`, and `dotnet tool run`, tokens that follow `--` are passed to the app that is being run by the command. For example: - -```dotnetcli -dotnet run --project ./myapp.csproj -- --message "Hello world!" - ^^ -``` - -In this example, the `--project` option is passed to the `dotnet run` command, and the `--message` option with its argument is passed as a command-line option to *myapp* when it runs. - -The `--` token is not always required for passing options to an app that you run by using `dotnet run`. Without the double-dash, the `dotnet run` command automatically passes on to the app being run any options that aren't recognized as applying to `dotnet run` itself or to MSBuild. So the following command lines are equivalent because `dotnet run` doesn't recognize the arguments and options: - -```dotnetcli -dotnet run -- quotes read --delay 0 --fg-color red -dotnet run quotes read --delay 0 --fg-color red -``` - -### Omission of the option-to-argument delimiter - -The .NET CLI doesn't support the POSIX convention that lets you omit the delimiter when you are specifying a single-character option alias. - -### Multiple arguments without repeating the option name - -The .NET CLI doesn't accept multiple arguments for one option without repeating the option name. - -### Boolean options - -In the .NET CLI, some Boolean options result in the same behavior when you pass `false` as when you pass `true`. This behavior results when .NET CLI code that implements the option only checks for the presence or absence of the option, ignoring the value. An example is `--no-restore` for the `dotnet build` command. Pass `no-restore false` and the restore operation will be skipped the same as when you specify `no-restore true` or `no-restore`. - -### Kebab case - -In some cases, the .NET CLI doesn't use kebab case for command, option, or argument names. For example, there is a .NET CLI option that is named [`--additionalprobingpath`](../../core/tools/dotnet.md#additionalprobingpath) instead of `--additional-probing-path`. - ## See also +* [Design guidance](design-guidance.md) * [Open-source CLI design guidance](https://clig.dev/) * [GNU standards](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html) * [System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/tab-completion.md b/docs/standard/commandline/tab-completion.md index 4336f7387418e..a160eb9a3d3cc 100644 --- a/docs/standard/commandline/tab-completion.md +++ b/docs/standard/commandline/tab-completion.md @@ -14,11 +14,11 @@ ms.topic: how-to [!INCLUDE [scl-preview](../../../includes/scl-preview.md)] -Apps that use `System.CommandLine` have built-in support for tab completion in certain shells. To enable it, the end user has to take a few steps once per shell. Once the user does this, tab completion is automatic for static values in your app, such as enum values or values you define by calling [FromAmong](define-commands.md#list-valid-argument-values). You can also customize the tab completion by getting values dynamically at runtime. +Apps that use `System.CommandLine` have built-in support for tab completion in certain shells. To enable it, the end user must take a few steps once per shell. Once this is done, tab completion is automatic for static values in your app, such as enum values or values defined by calling . You can also customize tab completion by providing values dynamically at runtime. ## Enable tab completion -On the machine where you'd like to enable tab completion, do the following steps. +On the machine where you want to enable tab completion, follow these steps. For the .NET CLI: @@ -28,7 +28,7 @@ For other command-line apps built on `System.CommandLine`: * Install the [`dotnet-suggest`](https://nuget.org/packages/dotnet-suggest) global tool. -* Add the appropriate shim script to your shell profile. You may have to create a shell profile file. The shim script forwards completion requests from your shell to the `dotnet-suggest` tool, which delegates to the appropriate `System.CommandLine`-based app. +* Add the appropriate shim script to your shell profile. You may need to create a shell profile file. The shim script forwards completion requests from your shell to the `dotnet-suggest` tool, which delegates them to the appropriate `System.CommandLine`-based app. * For `bash`, add the contents of [*dotnet-suggest-shim.bash*](https://github.com/dotnet/command-line-api/blob/main/src/System.CommandLine.Suggest/dotnet-suggest-shim.bash) to *~/.bash_profile*. @@ -40,26 +40,28 @@ For other command-line apps built on `System.CommandLine`: echo $profile ``` -Once the user's shell is set up, completions will work for all apps that are built by using `System.CommandLine`. +* Register the app by calling `dotnet-suggest register --command-path $executableFilePath`, where `$executableFilePath` is the path to the app's executable file. + +Once the user's shell is set up and the executable is registered, completions will work for all apps that are built by using `System.CommandLine`. For *cmd.exe* on Windows (the Windows Command Prompt) there is no pluggable tab completion mechanism, so no shim script is available. For other shells, [look for a GitHub issue that is labeled `Area-Completions`](https://github.com/dotnet/command-line-api/issues?q=is%3Aissue+is%3Aopen+label%3A%22Area-Completions%22). If you don't find an issue, you can [open a new one](https://github.com/dotnet/command-line-api/issues). ## Get tab completion values at run-time -The following code shows an app that gets values for tab completion dynamically at runtime. The code gets a list of the next two weeks of dates following the current date. The list is provided to the `--date` option by calling `AddCompletions`: +The following code shows an app that retrieves values for tab completion dynamically at runtime. The code gets a list of the next two weeks of dates following the current date. The list is provided to the `--date` option by calling `CompletionSources.Add`: :::code language="csharp" source="snippets/tab-completion/csharp/Program.cs" id="all" ::: -The values shown when the tab key is pressed are provided as `CompletionItem` instances: +The values shown when the Tab key is pressed are provided as `CompletionItem` instances: :::code language="csharp" source="snippets/tab-completion/csharp/Program.cs" id="completionitem" ::: The following `CompletionItem` properties are set: * `Label` is the completion value to be shown. -* `SortText` ensures that the values in the list are presented in the right order. It's set by converting `i` to a two-digit string, so that sorting is based on 01, 02, 03, and so on, through 14. If you don't set this parameter, sorting is based on the `Label`, which in this example is in short date format and won't sort correctly. +* `SortText` ensures that the values in the list are presented in the correct order. It is set by converting `i` to a two-digit string, so that sorting is based on 01, 02, 03, and so on, through 14. If you do not set this parameter, sorting is based on the `Label`, which in this example is in short date format and will not sort correctly. -There are other `CompletionItem` properties, such as `Documentation` and `Detail`, but they aren't used yet in `System.CommandLine`. +There are other `CompletionItem` properties, such as `Documentation` and `Detail`, but they are not yet used in `System.CommandLine`. The dynamic tab completion list created by this code also appears in help output: diff --git a/docs/standard/commandline/use-middleware.md b/docs/standard/commandline/use-middleware.md deleted file mode 100644 index 238992aeb282b..0000000000000 --- a/docs/standard/commandline/use-middleware.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: How to use middleware in System.CommandLine -description: "Learn how to use middleware for the System.CommandLine library." -ms.date: 04/07/2022 -no-loc: [System.CommandLine] -helpviewer_keywords: - - "command line interface" - - "command line" - - "System.CommandLine" -ms.topic: how-to ---- -# How to use middleware in System.CommandLine - -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] - -This article explains how to work with middleware in command-line apps that are built with the `System.CommandLine` library. Use of middleware is an advanced topic that most `System.CommandLine` users won't need to consider. - -## Introduction to middleware - -While each command has a handler that `System.CommandLine` will route to based on input, there's also a mechanism for short-circuiting or altering the input before your application logic is invoked. In between parsing and invocation, there's a chain of responsibility, which you can customize. A number of built-in features of `System.CommandLine` make use of this capability. This is how the `--help` and `--version` options short-circuit calls to your handler. - -Each call in the pipeline can take action based on the and return early, or choose to call the next item in the pipeline. The `ParseResult` can even be replaced during this phase. The last call in the chain is the handler for the specified command. - -## Add to the middleware pipeline - -You can add a call to this pipeline by calling . Here's an example of code that enables a custom [directive](syntax.md#directives). After creating a root command named `rootCommand`, the code as usual adds options, arguments, and handlers. Then the middleware is added: - -:::code language="csharp" source="snippets/use-middleware/csharp/Program.cs" id="middleware" ::: - -In the preceding code, the middleware writes out "Hi!" if the directive `[just-say-hi]` is found in the parse result. When this happens, the command's normal handler isn't invoked. It isn't invoked because the middleware doesn't call the `next` delegate. - -In the example, `context` is , a singleton structure that acts as the "root" of the entire command-handling process. This is the most powerful structure in `System.CommandLine`, in terms of capabilities. There are two main uses for it in middleware: - -* It provides access to the , , , and to retrieve dependencies that a middleware requires for its custom logic. -* You can set the or properties in order to terminate command processing in a short-circuiting manner. An example is the `--help` option, which is implemented in this manner. - -Here's the complete program, including required `using` directives. - -:::code language="csharp" source="snippets/use-middleware/csharp/Program.cs" id="all" ::: - -Here's an example command line and resulting output from the preceding code: - -```console -myapp [just-say-hi] --delay 42 --message "Hello world!" -``` - -```output -Hi! -``` - -## See also - -[System.CommandLine overview](index.md) diff --git a/includes/scl-preview.md b/includes/scl-preview.md index fbd02ffc3f820..6c26d2ff678ce 100644 --- a/includes/scl-preview.md +++ b/includes/scl-preview.md @@ -5,5 +5,5 @@ ms.date: 05/22/2022 ms.topic: include --- > [!IMPORTANT] -> `System.CommandLine` is currently in PREVIEW, and this documentation is for version 2.0 beta 4. +> `System.CommandLine` is currently in PREVIEW, and this documentation is for version 2.0 beta 5. > Some information relates to prerelease product that may be substantially modified before it's released. Microsoft makes no warranties, express or implied, with respect to the information provided here.