diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 32c9f61c..ff7eb128 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,9 +11,11 @@ updates: patterns: - "*" ignore: - # 4.3 is the latest version that can work with the .Net 6.0 SDK. + # 4.3.x is the latest version that can work with the .Net 6.0 SDK for both of these. - dependency-name: "Microsoft.CodeAnalysis.CSharp" update-types: ["version-update:semver-major", "version-update:semver-minor"] + - dependency-name: "Microsoft.CodeAnalysis.Workspaces" + update-types: ["version-update:semver-major", "version-update:semver-minor"] - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/README.md b/README.md index 5228ab48..9a71c3dc 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,9 @@ when [source generation](docs/SourceGeneration.md) is used. The .Net 7.0 version has additional support for `required` properties, and can utilize `ISpanParsable` and `IParsable` for argument value conversions. +An assembly built for .Net 8.0 is also provided; this has no additional functionality over the +.Net 7.0 version, but is provided to ensure optimal compatibility and performance. + ## Building and testing To build Ookii.CommandLine, make sure you have the following installed: diff --git a/docs/Arguments.md b/docs/Arguments.md index b3908b8d..2a303692 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -131,6 +131,26 @@ value1 -Positional1 value2 This is because `-Positional1` is assigned to twice; first by position, and then by name. Duplicate arguments cause an error by default, though this can be changed. +## The `--` argument + +Optionally, when an argument is encountered that consists only of `--` without a name following it, +this indicates that all following values must be treated as positional values, even if they begin +with an argument name prefix. + +For example, take the following command line: + +```text +value1 -- --value2 +``` + +In this example, the second positional argument would be set to the value "--value2". If there is +an argument named "value2", it would not be set. + +This behavior is disabled by default, but can be enabled using the +[`ParseOptionsAttribute.PrefixTermination`][] or [`ParseOptions.PrefixTermination`][] property. It +can be used with both the default parsing mode and long/short mode. Alternatively, you can also set +it so that the `--` argument will [cancel parsing](DefiningArguments.md#arguments-that-cancel-parsing). + ## Required arguments A command line argument that is required must be supplied on all invocations of the application. If @@ -289,33 +309,32 @@ upgrade code that relied on a [`TypeConverter`][]. ### Enumeration conversion -The [`EnumConverter`][] used for enumeration types relies on the [`Enum.Parse()`][] method. It uses case -insensitive conversion, and allows both the names and underlying value of the enumeration to be -used. This means that e.g. for the [`DayOfWeek`][] enumeration, "Monday", "monday", and "1" can all -be used to indicate [`DayOfWeek.Monday`][]. +The [`EnumConverter`][] used for enumeration types relies on the [`Enum.Parse()`][] method. Its +default behavior is to use case insensitive conversion, and to allow both the names and underlying +value of the enumeration to be used. This means that e.g. for the [`DayOfWeek`][] enumeration, +"Monday", "monday", and "1" can all be used to indicate [`DayOfWeek.Monday`][]. In the case of a numeric value, the converter does not check if the resulting value is valid for the enumeration type, so again for [`DayOfWeek`][], a value of "9" would be converted to `(DayOfWeek)9` even though there is no such value in the enumeration. To ensure the result is constrained to only the defined values of the enumeration, use the -[`ValidateEnumValueAttribute` validator](Validation.md). +[`ValidateEnumValueAttribute` validator](Validation.md). This validator can also be used to alter +the conversion behavior. You can enable case sensitivity with the +[`ValidateEnumValueAttribute.CaseSensitive`][] property, and disallow numeric values with the +[`ValidateEnumValueAttribute.AllowNumericValues`][] property. -The converter allows the use of comma-separated values, which will be combined using a bitwise or -operation. This is allowed regardless of whether or not the [`FlagsAttribute`][] attribute is present on -the enumeration, which can have unexpected results. Using the [`DayOfWeek`][] example again, -"Monday,Tuesday" would result in the value `DayOfWeek.Monday | DayOfWeek.Tuesday`, which is actually -equivalent to [`DayOfWeek.Wednesday`][]. +By default, the converter allows the use of comma-separated values, which will be combined using a +bitwise or operation. This is allowed regardless of whether or not the [`FlagsAttribute`][] +attribute is present on the enumeration, which can have unexpected results. Using the +[`DayOfWeek`][] example again, "Monday,Tuesday" would result in the value +`DayOfWeek.Monday | DayOfWeek.Tuesday`, which is actually equivalent to [`DayOfWeek.Wednesday`][]. -One way to avoid this is to use the following pattern validator, which ensures that the -string value before conversion does not contain a comma: - -```csharp -[ValidatePattern("^[^,]*$")] -``` +Comma-separated values can be disabled by using the +[`ValidateEnumValueAttribute.AllowCommaSeparatedValues`][] property. -You can also use a pattern like `"^[a-zA-Z]"` to ensure the value starts with a letter, to disallow -the use of numeric values entirely. +These properties of the [`ValidateEnumValueAttribute`][] attribute only work if the default +[`EnumConverter`][] is used; a custom converter may or may not check them. ### Multi-value and dictionary value conversion @@ -403,12 +422,12 @@ and case-sensitive argument names. For information on how to set these options, Next, let's take a look at how to [define arguments](DefiningArguments.md). -[`AllowDuplicateDictionaryKeysAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute.htm -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm -[`CommandLineArgument.AllowNull`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_AllowNull.htm -[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentException.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`AllowDuplicateDictionaryKeysAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`CommandLineArgument.AllowNull`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgument_AllowNull.htm +[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentException.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm [`CultureInfo.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.invariantculture [`CultureInfo`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo [`DateTime`]: https://learn.microsoft.com/dotnet/api/system.datetime @@ -416,28 +435,34 @@ Next, let's take a look at how to [define arguments](DefiningArguments.md). [`DayOfWeek.Wednesday`]: https://learn.microsoft.com/dotnet/api/system.dayofweek [`DayOfWeek`]: https://learn.microsoft.com/dotnet/api/system.dayofweek [`Enum.Parse()`]: https://learn.microsoft.com/dotnet/api/system.enum.parse -[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm +[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm [`FileInfo`]: https://learn.microsoft.com/dotnet/api/system.io.fileinfo [`FlagsAttribute`]: https://learn.microsoft.com/dotnet/api/system.flagsattribute [`Int32`]: https://learn.microsoft.com/dotnet/api/system.int32 [`IParsable`]: https://learn.microsoft.com/dotnet/api/system.iparsable-1 [`ISpanParsable`]: https://learn.microsoft.com/dotnet/api/system.ispanparsable-1 -[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm +[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm [`KeyValuePair`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.keyvaluepair-2 -[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm -[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm -[`MultiValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_MultiValueSeparatorAttribute.htm -[`ParseOptions.AllowWhiteSpaceValueSeparator`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator.htm -[`ParseOptions.ArgumentNameComparison`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm -[`ParseOptions.Culture`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_Culture.htm -[`ParseOptions.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_NameValueSeparators.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute.AllowWhiteSpaceValueSeparator`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_AllowWhiteSpaceValueSeparator.htm -[`ParseOptionsAttribute.CaseSensitive`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm -[`ParseOptionsAttribute.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm +[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm +[`MultiValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_MultiValueSeparatorAttribute.htm +[`ParseOptions.AllowWhiteSpaceValueSeparator`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator.htm +[`ParseOptions.ArgumentNameComparison`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm +[`ParseOptions.Culture`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_Culture.htm +[`ParseOptions.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_NameValueSeparators.htm +[`ParseOptions.PrefixTermination`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_PrefixTermination.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute.AllowWhiteSpaceValueSeparator`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_AllowWhiteSpaceValueSeparator.htm +[`ParseOptionsAttribute.CaseSensitive`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm +[`ParseOptionsAttribute.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators.htm +[`ParseOptionsAttribute.PrefixTermination`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_PrefixTermination.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm [`String`]: https://learn.microsoft.com/dotnet/api/system.string [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri -[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm -[NullArgumentValue_0]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`ValidateEnumValueAttribute.AllowCommaSeparatedValues`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowCommaSeparatedValues.htm +[`ValidateEnumValueAttribute.AllowNumericValues`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowNumericValues.htm +[`ValidateEnumValueAttribute.CaseSensitive`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_CaseSensitive.htm +[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm +[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm +[NullArgumentValue_0]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index e6e84441..14ca01b2 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,5 +1,38 @@ # What’s new in Ookii.CommandLine +## Ookii.CommandLine 4.1 (2024-01-26) + +- Support for [using a `--` argument](Arguments.md#the----argument) to escape argument names for the + remaining arguments, or to cancel parsing. This can be enabled using + [`ParseOptions.PrefixTermination`][] or [`ParseOptionsAttribute.PrefixTermination`][]. +- Ignore unknown arguments by using the new [`CommandLineParser.UnknownArgument`][] event. +- The [`ValidateEnumValueAttribute`][] has additional properties to control how + [enumeration values are parsed](Arguments.md#enumeration-conversion): + [`CaseSensitive`][CaseSensitive_1], [`AllowNumericValues`][], and [`AllowCommaSeparatedValues`][]. + - The [`EnumConverter`][] now also checks the + [`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`][] property, if the attribute is + present on the argument, so that error messages from the converter and validator are consistent. +- Support for passing a cancellation token to the [`CommandManager.RunCommandAsync()`][] method. + Tasks can access this token by implementing the [`IAsyncCancelableCommand`][] interface. The + [`AsyncCommandBase`][] class provides support as well. +- Usage help improvements: + - Support for [custom default value formatting](UsageHelp.md#default-values), using + [`CommandLineArgumentAttribute.DefaultValueFormat`][]. + - Add [`LineWrappingTextWriter.IndentAfterEmptyLine`][] and [`UsageWriter.IndentAfterEmptyLine`][] + properties, which allow for proper formatting of [argument descriptions with blank lines](UsageHelp.md#descriptions-with-blank-lines) + using the default usage help format. + - [Add a footer](UsageHelp.md#usage-help-footer) to the usage help with the + [`UsageFooterAttribute`][] attribute. + - Some localizable text that could previously only be customized by deriving from the + [`UsageWriter`][] class can now also be customized with the [`LocalizedStringProvider`][] class, + so you only need to derive from [`LocalizedStringProvider`][] to customize all user-facing + strings. +- Provide [helper methods](Utilities.md#virtual-terminal-support) in the [`VirtualTerminal`][] class + for writing text with VT formatting to the standard output or error streams. +- Provide extension methods for [`StandardStream`][] in the [`StandardStreamExtensions`][] class. +- Emit a warning if a class isn't using the [`GeneratedParserAttribute`][] when it could, with an + automatic code fix to easily apply it. + ## Ookii.CommandLine 4.0.1 (2023-09-19) - Fix an issue where arguments defined by methods could not have aliases. @@ -218,38 +251,59 @@ and usage. Upgrading an existing project that is using Ookii.CommandLine 1.0 to Ookii.CommandLine 2.0 or newer may require substantial code changes and may change how command lines are parsed. -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`AllowCommaSeparatedValues`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowCommaSeparatedValues.htm +[`AllowNumericValues`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowNumericValues.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm [`AssemblyTitleAttribute`]: https://learn.microsoft.com/dotnet/api/system.reflection.assemblytitleattribute -[`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp.htm -[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm -[`CommandOptions.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_IsPosix.htm +[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm +[`CommandLineArgumentAttribute.DefaultValueFormat`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValueFormat.htm +[`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp.htm +[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm +[`CommandLineParser.UnknownArgument`]: https://www.ookii.org/docs/commandline-4.1/html/E_Ookii_CommandLine_CommandLineParser_UnknownArgument.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm +[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`CommandOptions.IsPosix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_IsPosix.htm [`CultureInfo.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.invariantculture +[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs -[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`IAsyncCancelableCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_IAsyncCancelableCommand.htm [`IParsable`]: https://learn.microsoft.com/dotnet/api/system.iparsable-1 [`ISpanParsable`]: https://learn.microsoft.com/dotnet/api/system.ispanparsable-1 -[`LineWrappingTextWriter.ToString()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ToString.htm -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm -[`ParseOptions.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_IsPosix.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`LineWrappingTextWriter.IndentAfterEmptyLine`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_IndentAfterEmptyLine.htm +[`LineWrappingTextWriter.ToString()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ToString.htm +[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`ParseOptions.IsPosix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_IsPosix.htm +[`ParseOptions.PrefixTermination`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_PrefixTermination.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm +[`ParseOptionsAttribute.PrefixTermination`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_PrefixTermination.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm [`ReadOnlySpan`]: https://learn.microsoft.com/dotnet/api/system.readonlyspan-1 -[`ResetIndentAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndentAsync.htm +[`ResetIndentAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndentAsync.htm +[`StandardStream`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_StandardStream.htm +[`StandardStreamExtensions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_StandardStreamExtensions.htm [`StringWriter`]: https://learn.microsoft.com/dotnet/api/system.io.stringwriter [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[`Wrapping`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm -[Flush()_0]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_Flush_1.htm -[Parse()_6]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm -[UsageWriter_1]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[WriteAsync()_4]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync.htm -[WriteLineAsync()_5]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteLineAsync.htm +[`UsageFooterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageFooterAttribute.htm +[`UsageWriter.IndentAfterEmptyLine`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IndentAfterEmptyLine.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage.htm +[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[`VirtualTerminal`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_VirtualTerminal.htm +[`Wrapping`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm +[CaseSensitive_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_CaseSensitive.htm +[Flush()_0]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_Flush_1.htm +[Parse()_6]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse.htm +[Parse()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[UsageWriter_1]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[WriteAsync()_4]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync.htm +[WriteLineAsync()_5]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteLineAsync.htm diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 5658ddbf..f1d8aa53 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -379,10 +379,11 @@ public bool Help { get; set; } Note that this property will never be set to true by the [`CommandLineParser`][], since no instance will be created if the argument is supplied. -If you set the [`CancelParsing`][CancelParsing_1] property to [`CancelMode.Success`][], parsing is stopped, and the rest -of the command line is not process, but parsing will complete successfully. If all the required -arguments have been specified before that point, the [`CommandLineParser.Parse()`][] method and -various helper methods will return an instance of the arguments type. +If you set the [`CancelParsing`][CancelParsing_1] property to [`CancelMode.Success`][], parsing is +stopped, and the rest of the command line is not processed, but parsing will complete successfully. +If all the required arguments have been specified before that point, the +[`CommandLineParser.Parse()`][] method and various helper methods will return an instance of the +arguments type. The remaining arguments that were not parsed are available in the [`ParseResult.RemainingArguments`][] property. These are available for [`CancelMode.Abort`][], [`CancelMode.Success`][], and if parsing @@ -392,6 +393,10 @@ encountered an error. line processor, for example a child application, or a subcommand. See for example the [top-level arguments sample](../src/Samples/TopLevelArguments). +The `--` argument can also be used to cancel parsing and return success, by setting the +[`ParseOptionsAttribute.PrefixTermination`][] or [`ParseOptions.PrefixTermination`][] property to +[`PrefixTerminationMode.CancelWithSuccess`][]. + ## Using methods You can also apply the [`CommandLineArgumentAttribute`][] to a public static method. Method @@ -470,13 +475,21 @@ partial class Arguments ### Long/short mode -To enable [long/short mode](Arguments.md#longshort-mode), you typically want to set three options -if you want to mimic typical POSIX conventions: the mode itself, case sensitive argument names, -and dash-case [name transformation](#name-transformation). This can be done with either the -[`ParseOptionsAttribute`][] attribute or the [`ParseOptions`][] class. +When enabling [long/short mode](Arguments.md#longshort-mode), you may want to also set several +related options if you want to mimic typical POSIX conventions: long/short mode itself, case +sensitive argument names, and dash-case [name transformation](#name-transformation). This can be +done with either the [`ParseOptionsAttribute`][] attribute or the [`ParseOptions`][] class. + +A convenient [`IsPosix`][IsPosix_2] property is provided on either class, that sets all relevant +options when set to true. Using `[ParseOptions(IsPosix = true)]` is equivalent to manually setting +the following properties. -A convenient [`IsPosix`][IsPosix_2] property is provided on either class, that sets all relevant options when -set to true. +```csharp +[ParseOptions(Mode = ParsingMode.LongShort, + CaseSensitive = true, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionNameTransform = NameTransform.DashCase)] +``` When using long/short mode, the name derived from the member name, or the explicit name set by the [`CommandLineArgumentAttribute`][] attribute is the long name. @@ -504,15 +517,6 @@ partial class MyArguments } ``` -Using `[ParseOptions(IsPosix = true)]` is equivalent to manually setting the following properties. - -```csharp -[ParseOptions(Mode = ParsingMode.LongShort, - CaseSensitive = true, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionNameTransform = NameTransform.DashCase)] -``` - In this example, the `FileName` property defines a required positional argument with the long name `--file-name` and the short name `-f`. The `Foo` property defines an argument with the long name `--foo` and the explicit short name `-F`, which is distinct from `-f` because case sensitivity is @@ -624,52 +628,55 @@ disable either automatic argument using the [`ParseOptions`][] class. Next, we'll take a look at how to [parse the arguments we've defined](ParsingArguments.md) -[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm -[`CancelMode.Abort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CancelMode.None`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CancelMode`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm -[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm -[`CommandLineArgumentAttribute.IsRequired`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm -[`CommandLineArgumentAttribute.IsShort`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm -[`CommandLineArgumentAttribute.Position`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm -[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm -[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm -[`CommandLineParser.HelpRequested`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[`DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_AliasAttribute.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`CancelMode.Abort`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CancelMode.None`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CancelMode`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`CommandLineArgumentAttribute.IsRequired`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm +[`CommandLineArgumentAttribute.IsShort`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm +[`CommandLineArgumentAttribute.Position`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm +[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser.HelpRequested`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[`DefaultValue`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Dictionary`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.dictionary-2 -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm [`ICollection`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.icollection-1 [`IDictionary`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.idictionary-2 -[`IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`IsPositional`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm [`List`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1 -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`ParseOptions.ArgumentNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform.htm -[`ParseOptions.AutoPrefixAliases`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_AutoPrefixAliases.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute.AutoPrefixAliases`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm -[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`ParseOptions.ArgumentNameTransform`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform.htm +[`ParseOptions.AutoPrefixAliases`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_AutoPrefixAliases.htm +[`ParseOptions.PrefixTermination`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_PrefixTermination.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute.AutoPrefixAliases`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases.htm +[`ParseOptionsAttribute.PrefixTermination`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_PrefixTermination.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm +[`PrefixTerminationMode.CancelWithSuccess`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_PrefixTerminationMode.htm [`ReadOnlySpan`]: https://learn.microsoft.com/dotnet/api/system.readonlyspan-1 -[`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ShortAliasAttribute.htm +[`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ShortAliasAttribute.htm [`String`]: https://learn.microsoft.com/dotnet/api/system.string [`System.ComponentModel.DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`TypeDescriptor.GetConverter()`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typedescriptor.getconverter -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm -[`WrappedDefaultTypeConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_WrappedDefaultTypeConverter_1.htm -[CancelParsing_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[DefaultValue_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[IsPosix_2]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm -[Position_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[`WrappedDefaultTypeConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_WrappedDefaultTypeConverter_1.htm +[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm +[CancelParsing_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[DefaultValue_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[IsPosix_2]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm +[Parse()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[Position_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm diff --git a/docs/Migrating.md b/docs/Migrating.md index d075041b..28c595ba 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -137,68 +137,68 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - The [`LineWrappingTextWriter`][] class does not count virtual terminal sequences as part of the line length by default. -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm -[`ArgumentParsed`]: https://www.ookii.org/docs/commandline-4.0/html/E_Ookii_CommandLine_CommandLineParser_ArgumentParsed.htm -[`ArgumentParsedEventArgs`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ArgumentParsedEventArgs.htm -[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm -[`CancelMode`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm -[`CommandInfo`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandInfo.htm -[`CommandLineArgument.DictionaryInfo`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_DictionaryInfo.htm -[`CommandLineArgument.ElementType`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_ElementType.htm -[`CommandLineArgument.Kind`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_Kind.htm -[`CommandLineArgument.MultiValueInfo`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_MultiValueInfo.htm -[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[`CommandLineParser.HelpRequested`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_ParseWithErrorHandling.htm -[`CommandLineParser.WriteUsage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_WriteUsage.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`CommandManager.CreateCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm -[`CommandManager.RunCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`CommandNameComparison`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparison.htm -[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandOptions.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`ArgumentParsed`]: https://www.ookii.org/docs/commandline-4.1/html/E_Ookii_CommandLine_CommandLineParser_ArgumentParsed.htm +[`ArgumentParsedEventArgs`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ArgumentParsedEventArgs.htm +[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm +[`CancelMode`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandInfo`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandInfo.htm +[`CommandLineArgument.DictionaryInfo`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgument_DictionaryInfo.htm +[`CommandLineArgument.ElementType`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgument_ElementType.htm +[`CommandLineArgument.Kind`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgument_Kind.htm +[`CommandLineArgument.MultiValueInfo`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgument_MultiValueInfo.htm +[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineParser.HelpRequested`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_ParseWithErrorHandling.htm +[`CommandLineParser.WriteUsage()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_WriteUsage.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`CommandManager.CreateCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm +[`CommandManager.RunCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`CommandNameComparison`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparison.htm +[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandOptions.htm [`CultureInfo.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.invariantculture [`CurrentCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.currentculture -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm -[`HelpRequested`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm -[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[`ICommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommand.htm -[`ICommandWithCustomParsing.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse.htm -[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`HelpRequested`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm +[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm +[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm +[`ICommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommand.htm +[`ICommandWithCustomParsing.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse.htm +[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm [`IComparer`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.icomparer-1 [`ImmutableArray`]: https://learn.microsoft.com/dotnet/api/system.collections.immutable.immutablearray-1 -[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm -[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm -[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm +[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm +[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm +[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 -[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Commands.htm -[`Ookii.CommandLine.Conversion`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Conversion.htm -[`ParseOptions.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_NameValueSeparators.htm -[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators.htm +[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-4.1/html/N_Ookii_CommandLine_Commands.htm +[`Ookii.CommandLine.Conversion`]: https://www.ookii.org/docs/commandline-4.1/html/N_Ookii_CommandLine_Conversion.htm +[`ParseOptions.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_NameValueSeparators.htm +[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators.htm [`ReadOnlyCollection`]: https://learn.microsoft.com/dotnet/api/system.collections.objectmodel.readonlycollection-1 [`ReadOnlyMemory`]: https://learn.microsoft.com/dotnet/api/system.readonlymemory-1 [`StringComparison`]: https://learn.microsoft.com/dotnet/api/system.stringcomparison -[`TextFormat`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_TextFormat.htm +[`TextFormat`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_TextFormat.htm [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute -[`UsageHelpRequest.SyntaxOnly`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageHelpRequest.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm -[`WrappedDefaultTypeConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_WrappedDefaultTypeConverter_1.htm -[ArgumentNameComparison_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm -[CommandLineParser.Parse()_2]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm -[Parse()_5]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[Parse()_6]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm +[`UsageHelpRequest.SyntaxOnly`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageHelpRequest.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm +[`WrappedDefaultTypeConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_WrappedDefaultTypeConverter_1.htm +[ArgumentNameComparison_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm +[CommandLineParser.Parse()_2]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm +[Parse()_5]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[Parse()_6]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm diff --git a/docs/Ookii.CommandLine.shfbproj b/docs/Ookii.CommandLine.shfbproj index e4b42177..6a37fa30 100644 --- a/docs/Ookii.CommandLine.shfbproj +++ b/docs/Ookii.CommandLine.shfbproj @@ -45,7 +45,7 @@ https://github.com/SvenGroot/Ookii.CommandLine Copyright &#169%3b Sven Groot %28Ookii.org%29 - Ookii.CommandLine 4.0 documentation + Ookii.CommandLine 4.1 documentation MemberName Default2022 C#, Visual Basic, Visual Basic Usage, Managed C++ diff --git a/docs/ParsingArguments.md b/docs/ParsingArguments.md index a547bdde..bcb827ef 100644 --- a/docs/ParsingArguments.md +++ b/docs/ParsingArguments.md @@ -106,9 +106,10 @@ and create your own error message. The generated [`Parse()`][Parse()_7] methods and the static [`Parse()`][Parse()_1] method and their overloads will likely be sufficient for most use cases. However, sometimes you may want even -more fine-grained control. This includes the ability to handle the [`ArgumentParsed`][] and -[`DuplicateArgument`][DuplicateArgument_0] events, and to get additional information about the -arguments using the [`Arguments`][Arguments_0] property or the [`GetArgument`][] function. +more fine-grained control. This includes the ability to handle the [`ArgumentParsed`][], +[`UnknownArgument`][] and [`DuplicateArgument`][DuplicateArgument_0] events, and to get additional +information about the arguments using the [`Arguments`][Arguments_0] property or the +[`GetArgument`][] function. In this case, you can manually create an instance of the [`CommandLineParser`][] class. Then, call the instance [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] or [`Parse()`][Parse()_5] method. @@ -138,6 +139,27 @@ if (arguments == null) } ``` +Or, you could use this to handle the [`UnknownArgument`][] event to collect a list of unrecognized +arguments: + +```csharp +var unknownArguments = new List(); +var parser = MyArguments.CreateParser(); +parser.UnknownArgument += (_, e) => +{ + // Note: in long/short mode, this may not have the desired effect for a combined switch argument + // where one of the switches is unknown. + unknownArguments.Add(e.Token); + e.Ignore = true; +}; + +var arguments = parser.ParseWithErrorHandling(); +if (arguments == null) +{ + return 1; +} +``` + The status will be set to [`ParseStatus.Canceled`][] if parsing was canceled with [`CancelMode.Abort`][]. You can also use the [`ParseResult.ArgumentName`][] property to determine which argument canceled @@ -208,38 +230,39 @@ methods only. Next, we'll take a look at [generating usage help](UsageHelp.md). -[`ArgumentParsed`]: https://www.ookii.org/docs/commandline-4.0/html/E_Ookii_CommandLine_CommandLineParser_ArgumentParsed.htm -[`CancelMode.Abort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[`CommandLineArgumentErrorCategory`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm -[`CommandLineArgumentException.Category`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm -[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentException.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`ArgumentParsed`]: https://www.ookii.org/docs/commandline-4.1/html/E_Ookii_CommandLine_CommandLineParser_ArgumentParsed.htm +[`CancelMode.Abort`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineArgumentErrorCategory`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`CommandLineArgumentException.Category`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm +[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentException.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs -[`Error`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_Error.htm -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm -[`GetArgument`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_GetArgument.htm -[`HelpRequested`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`ParseOptions.StringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_StringProvider.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm -[`ParseResult.ArgumentName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_ArgumentName.htm -[`ParseResult.LastException`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_LastException.htm -[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm -[`ParseStatus.Canceled`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseStatus.htm -[`ParseStatus.Error`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseStatus.htm -[`ParseStatus.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseStatus.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[Arguments_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_Arguments.htm -[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm -[DuplicateArgument_0]: https://www.ookii.org/docs/commandline-4.0/html/E_Ookii_CommandLine_CommandLineParser_DuplicateArgument.htm -[Parse()_5]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`Error`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_Error.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`GetArgument`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_GetArgument.htm +[`HelpRequested`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`ParseOptions.StringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_StringProvider.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParseResult.ArgumentName`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseResult_ArgumentName.htm +[`ParseResult.LastException`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseResult_LastException.htm +[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm +[`ParseStatus.Canceled`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseStatus.htm +[`ParseStatus.Error`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseStatus.htm +[`ParseStatus.Success`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseStatus.htm +[`UnknownArgument`]: https://www.ookii.org/docs/commandline-4.1/html/E_Ookii_CommandLine_CommandLineParser_UnknownArgument.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[Arguments_0]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineParser_Arguments.htm +[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm +[DuplicateArgument_0]: https://www.ookii.org/docs/commandline-4.1/html/E_Ookii_CommandLine_CommandLineParser_DuplicateArgument.htm +[Parse()_5]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[Parse()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index 4bd9596e..55a8c666 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -41,6 +41,15 @@ A few restrictions apply to projects that use Ookii.CommandLine's source generat Generally, it's recommended to use source generation unless you cannot meet these requirements. +To encourage the use of source generation, Ookii.CommandLine also includes an analyzer that will +emit [a warning](SourceGenerationDiagnostics.md#ocl0040) if a class is found that contains any +public property or method with the [`CommandLineArgumentAttribute`][] attribute and meets the +requirements listed above, but does not have the [`GeneratedParserAttribute`][]. A code fix, +accessible with lightbulb UI in Visual Studio, that applies the attribute and makes the class +partial if necessary, is available in this case. + +If you don't want to or cannot use source generation, you can simply disable this warning. + ## Generating a parser To use source generation to determine the command line arguments defined by a class, apply the @@ -278,23 +287,23 @@ return manager.RunCommand() ?? 1; Next, we will take a look at several [utility classes](Utilities.md) provided, and used, by Ookii.CommandLine. -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm -[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm -[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`GeneratedCommandManagerAttribute.AssemblyNames`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames.htm -[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm -[`GeneratedConverterNamespaceAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute.htm -[`GeneratedParserAttribute.GenerateParseMethods`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_GeneratedParserAttribute_GenerateParseMethods.htm -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm -[`IParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_IParser_1.htm -[`IParserProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_IParserProvider_1.htm -[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`GeneratedCommandManagerAttribute.AssemblyNames`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedConverterNamespaceAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute.htm +[`GeneratedParserAttribute.GenerateParseMethods`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_GeneratedParserAttribute_GenerateParseMethods.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`IParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_IParser_1.htm +[`IParserProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_IParserProvider_1.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm [`Type`]: https://learn.microsoft.com/dotnet/api/system.type -[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm -[DefaultValue_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm +[DefaultValue_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index b88eaf3d..2474ddba 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -34,7 +34,7 @@ For example, the following code triggers this error: [GeneratedParser] partial struct Arguments // ERROR: The type must be a class. { - [CommandLineAttribute] + [CommandLineArgument] public string? Argument { get; set; } } ``` @@ -52,7 +52,7 @@ For example, the following code triggers this error: [GeneratedParser] class Arguments // ERROR: The class must be partial { - [CommandLineAttribute] + [CommandLineArgument] public string? Argument { get; set; } } ``` @@ -70,7 +70,7 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments // ERROR: The class must not be generic { - [CommandLineAttribute] + [CommandLineArgument] public T? Argument { get; set; } } ``` @@ -90,7 +90,7 @@ class SomeClass [GeneratedParser] public partial class Arguments // ERROR: The class must not be nested { - [CommandLineAttribute] + [CommandLineArgument] public T? Argument { get; set; } } } @@ -108,7 +108,7 @@ For example, the following code triggers this error: partial class Arguments { // ERROR: Argument using an array rank other than one. - [CommandLineAttribute] + [CommandLineArgument] public string[,]? Argument { get; set; } } ``` @@ -128,7 +128,7 @@ For example, the following code triggers this error: partial class Arguments { // ERROR: Property must use a public set accessor. - [CommandLineAttribute] + [CommandLineArgument] public string? Argument { get; private set; } } ``` @@ -151,7 +151,7 @@ For example, the following code triggers this error: partial class Arguments { // ERROR: Argument type must have a converter - [CommandLineAttribute] + [CommandLineArgument] public Socket? Argument { get; set; } } ``` @@ -171,7 +171,7 @@ For example, the following code triggers this error: partial class Arguments { // ERROR: the method has an unrecognized parameter - [CommandLineAttribute] + [CommandLineArgument] public static void Argument(string value, int value2); } ``` @@ -191,7 +191,7 @@ For example, the following code triggers this error: partial class Arguments { // ERROR: The property uses init but is not required - [CommandLineAttribute(IsRequired = true)] + [CommandLineArgument(IsRequired = true)] public string? Argument { get; init; } } ``` @@ -203,7 +203,7 @@ To fix this error, either use a regular `set` accessor, or if using .Net 7.0 or [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] public required string Argument { get; init; } } ``` @@ -248,11 +248,11 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - [CommandLineAttribute(IsPositional = true)] + [CommandLineArgument(IsPositional = true)] public string[]? Argument1 { get; set; } // ERROR: Argument2 comes after Argument1, which is multi-value. - [CommandLineAttribute(IsPositional = true] + [CommandLineArgument(IsPositional = true] public string? Argument2 { get; set; } } ``` @@ -269,11 +269,11 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - [CommandLineAttribute(IsPositional = true)] + [CommandLineArgument(IsPositional = true)] public string? Argument1 { get; set; } // ERROR: Required argument Argument2 comes after Argument1, which is optional. - [CommandLineAttribute(IsPositional = true)] + [CommandLineArgument(IsPositional = true)] public required string Argument2 { get; set; } } ``` @@ -325,7 +325,7 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [ArgumentConverter("MyNamespace.MyConverter")] // ERROR: Can't use a string type name. public CustomType? Argument { get; set; } } @@ -351,7 +351,7 @@ For example, the following code triggers this error: partial class Arguments { // ERROR: No long or short name (IsShort is false by default). - [CommandLineAttribute(IsLong = false)] + [CommandLineArgument(IsLong = false)] public string? Argument { get; set; } } ``` @@ -391,10 +391,10 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - [CommandLineAttribute(IsPositional = true)] + [CommandLineArgument(IsPositional = true)] public string? Argument1 { get; set; } - [CommandLineAttribute(Position = 0)] + [CommandLineArgument(Position = 0)] public string? Argument2 { get; set; } } ``` @@ -420,7 +420,7 @@ For example, the following code triggers this warning: [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [TypeConverter(typeof(MyNamespace.MyConverter)] // WARNING: TypeConverterAttribute is not used public CustomType? Argument { get; set; } } @@ -433,7 +433,7 @@ existing [`TypeConverter`][], you can use the [`WrappedTypeConverter`][] clas [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [ArgumentConverter(typeof(WrappedTypeConverter)] public CustomType? Argument { get; set; } } @@ -453,7 +453,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: the method must be public - [CommandLineAttribute] + [CommandLineArgument] private static void Argument(string value, int value2); } ``` @@ -472,7 +472,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: the property must be public - [CommandLineAttribute] + [CommandLineArgument] private string? Argument { get; set; } } ``` @@ -493,7 +493,7 @@ For example, the following code triggers this warning: [Command] partial class MyCommand // WARNING: The class doesn't implement ICommand { - [CommandLineAttribute] + [CommandLineArgument] public string? Argument { get; set; } } ``` @@ -520,7 +520,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: Default value is unused on a required argument. - [CommandLineAttribute(DefaultValue = "foo")] + [CommandLineArgument(DefaultValue = "foo")] public required string Argument { get; set; } } ``` @@ -541,7 +541,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: the argument will be required regardless of the value of IsRequired. - [CommandLineAttribute(IsRequired = false)] + [CommandLineArgument(IsRequired = false)] public required string Argument { get; set; } } ``` @@ -558,11 +558,11 @@ of the arguments, which should be avoided. [GeneratedParser] partial class Arguments { - [CommandLineAttribute(Position = 0)] + [CommandLineArgument(Position = 0)] public string? Argument1 { get; set; } // WARNING: Argument2 has the same position as Argument1. - [CommandLineAttribute(Position = 0)] + [CommandLineArgument(Position = 0)] public string? Argument2 { get; set; } } ``` @@ -590,7 +590,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: The short alias is not used since the argument has no short name. - [CommandLineAttribute] + [CommandLineArgument] [ShortAlias('a')] public string? Argument { get; set; } } @@ -613,7 +613,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: The long alias is not used since the argument has no long name. - [CommandLineAttribute(IsLong = false, IsShort = true)] + [CommandLineArgument(IsLong = false, IsShort = true)] [Alias("arg")] public string? Argument { get; set; } } @@ -638,7 +638,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: The argument is not hidden because it's positional. - [CommandLineAttribute(IsPositional = true, IsHidden = true)] + [CommandLineArgument(IsPositional = true, IsHidden = true)] public string? Argument { get; set; } } ``` @@ -667,7 +667,7 @@ For example, the following code triggers this warning: [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [AllowDuplicateDictionaryKeys] // WARNING: Ignored on non-dictionary arguments public string? Argument { get; set; } } @@ -685,7 +685,7 @@ For example, the following code triggers this warning: [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [ArgumentConverter(typeof(CustomKeyValuePairConverter))] [KeyValueSeparator(":")] // WARNING: Ignored on dictionary arguments with an explicit converter. public Dictionary? Argument { get; set; } @@ -703,7 +703,7 @@ For example, the following code triggers this warning: [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [MultiValueSeparator(",")] // WARNING: Ignored on non-multi-value arguments public string? Argument { get; set; } } @@ -725,7 +725,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: Name starts with a number. - [CommandLineAttribute("1Arg")] + [CommandLineArgument("1Arg")] public string? Argument { get; set; } } ``` @@ -751,7 +751,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: Argument has a short name so IsShort is ignored. - [CommandLineAttribute(ShortName = 'a', IsShort = false)] + [CommandLineArgument(ShortName = 'a', IsShort = false)] public string? Argument { get; set; } } ``` @@ -772,7 +772,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: No DescriptionAttribute on this member. - [CommandLineAttribute] + [CommandLineArgument] public string? Argument { get; set; } } ``` @@ -784,7 +784,7 @@ To fix this, write a concise description explaining the argument's purpose and u [GeneratedParser] partial class Arguments { - [CommandLineAttribute] + [CommandLineArgument] [Description("A description of the argument.")] public string? Argument { get; set; } } @@ -806,7 +806,7 @@ For example, the following code triggers this warning: [Command] partial class MyCommand : ICommand { - [CommandLineAttribute] + [CommandLineArgument] [Description("A description of the argument.")] public string? Argument { get; set; } } @@ -821,7 +821,7 @@ To fix this, write a concise description explaining the command's purpose, and a [Command] partial class MyCommand : ICommand { - [CommandLineAttribute] + [CommandLineArgument] [Description("A description of the argument.")] public string? Argument { get; set; } } @@ -843,7 +843,7 @@ For example, the following code triggers this warning: [ParentCommand(typeof(SomeCommand))] partial class MyCommand { - [CommandLineAttribute] + [CommandLineArgument] [Description("A description of the argument.")] public string? Argument { get; set; } } @@ -865,7 +865,7 @@ For example, the following code triggers this warning: [Command] partial class MyCommand : ICommand { - [CommandLineAttribute] + [CommandLineArgument] [Description("A description of the argument.")] public string? Argument { get; set; } } @@ -897,7 +897,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: Method call for property initializer is not supported for the usage help. - [CommandLineAttribute] + [CommandLineArgument] public string? Argument { get; set; } = GetDefaultValue(); private static int GetDefaultValue() @@ -908,9 +908,9 @@ partial class Arguments ``` This will not affect the actual value of the argument, since the property will not be set by the -[`CommandLineParser`][] if the [`CommandLineArgumentAttribute.DefaultValue`][] property is null. Therefore, -you can safely suppress this warning and include the relevant explanation of the default value in -the property's description manually, if desired. +[`CommandLineParser`][] if the [`CommandLineArgumentAttribute.DefaultValue`][] property is null. +Therefore, you can safely suppress this warning and include the relevant explanation of the default +value in the property's description manually, if desired. To avoid this warning, use one of the supported expression types, or use the [`CommandLineArgumentAttribute.DefaultValue`][] property. This warning will not be emitted if the @@ -922,52 +922,138 @@ Note that default values set by property initializers are only shown in the usag [`GeneratedParserAttribute`][] is used. When reflection is used, only [`CommandLineArgumentAttribute.DefaultValue`][] is supported. -[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm -[`AllowDuplicateDictionaryKeysAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute.htm -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm -[`CommandAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden.htm -[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm -[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp.htm -[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm -[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm -[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm -[`CommandLineArgumentAttribute.IsRequired`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm -[`CommandLineArgumentAttribute.IsShort`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm -[`CommandLineArgumentAttribute.Position`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm -[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm -[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm +### OCL0040 + +Command line arguments classes should use source generation. + +This warning is emitted for any class that contains members with the +[`CommandLineArgumentAttribute`][], but which does not have the [`GeneratedParserAttribute`][] +applied. Using [source generation](SourceGeneration.md) is recommended in all cases, unless you +cannot meet the requirements. + +This warning is not emitted if the project does not use C# 8 or later, or the class is abstract, +nested in another type, or has generic type arguments. + +For example, the following code triggers this warning: + +```csharp +// WARNING: The "Argument" property has the CommandLineArgumentAttribute, but the class does not +// have the GeneratedParserAttribute. +class Arguments +{ + [CommandLineArgument] + public string? Argument { get; set; } +} +``` + +A code fix is provided that lets you use the lightbulb UI in Visual Studio to quickly add the +[`GeneratedParserAttribute`][] to the class. This will also make the class `partial` if it isn't +already. + +If you cannot use source generation for some reason, you should disable this warning. + +### OCL0041 + +The [`ValidateEnumValueAttribute`][] attribute was applied to an argument whose type is not an +enumeration type, a nullable enumeration type, or an array or collection containing an enumeration +type. + +The [`ValidateEnumValueAttribute`][] attribute only supports enumeration types, and will throw an +exception at runtime if used to validate an argument whose type is not an enumeration. + +For example, the following code triggers this warning: + +```csharp +class Arguments +{ + // WARNING: String isn't an enumeration type. + [CommandLineArgument] + [ValidateEnumValue] + public string? Argument { get; set; } +} +``` + +To fix this warning, either remove the [`ValidateEnumValueAttribute`][] attribute or change the type +of the argument to an enumeration type. + +### OCL0042 + +An argument has the [`ArgumentConverterAttribute`][] set, and uses properties of the +[`ValidateEnumValueAttribute`][] that may not be supported by a custom converter. + +The [`CaseSensitive`][CaseSensitive_1], [`AllowCommaSeparatedValues`][], and +[`AllowNumericValues`][] properties of the [`ValidateEnumValueAttribute`][] attribute are not used +by the [`ValidateEnumValueAttribute`][] attribute itself, but instead alter the behavior of the +[`EnumConverter`][] class. If an argument uses a custom converter rather than the +[`EnumConverter`][], it is not guaranteed that these properties will have any effect. + +For example, the following code triggers this warning: + +```csharp +class Arguments +{ + // WARNING: ValidateEnumValueAttribute.CaseSensitive used with a custom argument converter. + [CommandLineArgument] + [ArgumentConverter(typeof(MyConverter))] + [ValidateEnumValue(CaseSensitive = true)] + public DayOfWeek Argument { get; set; } +} +``` + +To fix this warning, either use the default [`EnumConverter`][], or remove the properties. If the +custom converter does check the value of those properties, you can disable this warning. + +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_AliasAttribute.htm +[`AllowCommaSeparatedValues`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowCommaSeparatedValues.htm +[`AllowDuplicateDictionaryKeysAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute.htm +[`AllowNumericValues`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowNumericValues.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`CommandAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp.htm +[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm +[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`CommandLineArgumentAttribute.IsRequired`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm +[`CommandLineArgumentAttribute.IsShort`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm +[`CommandLineArgumentAttribute.Position`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm +[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute -[`GeneratedCommandManagerAttribute.AssemblyNames`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames.htm -[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm -[`GeneratedConverterNamespaceAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute.htm -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm -[`ICommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommand.htm -[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm -[`IsShort`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm -[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm -[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm -[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm -[`MultiValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_MultiValueSeparatorAttribute.htm -[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm -[`ParseOptions.ArgumentNameComparison`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm -[`ParseOptions.ArgumentNamePrefixes`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNamePrefixes.htm -[`ParseOptions.ArgumentNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform.htm -[`ParseOptions.Mode`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_Mode.htm -[`ParseOptionsAttribute.ArgumentNamePrefixes`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_ArgumentNamePrefixes.htm -[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParsingMode.htm -[`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ShortAliasAttribute.htm +[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm +[`GeneratedCommandManagerAttribute.AssemblyNames`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedConverterNamespaceAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`ICommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommand.htm +[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm +[`IsShort`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm +[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm +[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm +[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm +[`MultiValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_MultiValueSeparatorAttribute.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`ParseOptions.ArgumentNameComparison`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm +[`ParseOptions.ArgumentNamePrefixes`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNamePrefixes.htm +[`ParseOptions.ArgumentNameTransform`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform.htm +[`ParseOptions.Mode`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_Mode.htm +[`ParseOptionsAttribute.ArgumentNamePrefixes`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_ArgumentNamePrefixes.htm +[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParsingMode.htm +[`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ShortAliasAttribute.htm [`Type`]: https://learn.microsoft.com/dotnet/api/system.type [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter -[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm [`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute -[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm -[IsHidden_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm -[IsRequired_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm -[ShortName_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm +[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm +[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm +[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm +[CaseSensitive_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_CaseSensitive.htm +[IsHidden_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm +[IsRequired_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm +[ShortName_1]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm diff --git a/docs/Subcommands.md b/docs/Subcommands.md index 90597311..a965945d 100644 --- a/docs/Subcommands.md +++ b/docs/Subcommands.md @@ -164,6 +164,11 @@ partial class AsyncSleepCommand : AsyncCommandBase } ``` +To support cancellation, you can pass a [`CancellationToken`][] to the +[`CommandManager.RunCommandAsync()`][] method. This token can be accessed by a command if it +implements the [`IAsyncCancelableCommand`][] interface. If you use the [`AsyncCommandBase`][] class, +the token is available using the [`AsyncCommandBase.CancellationToken`][] property. + ### Multiple commands with common arguments You may have multiple commands that have one or more arguments in common. For example, you may have @@ -663,59 +668,62 @@ functionality. The next page will discuss Ookii.CommandLine's [source generation](SourceGeneration.md) in more detail. -[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm -[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm -[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm -[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandManager.GetCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm -[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm -[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`CommandNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm -[`CommandOptions.AutoCommandPrefixAliases`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoCommandPrefixAliases.htm -[`CommandOptions.AutoVersionCommand`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand.htm -[`CommandOptions.CommandFilter`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter.htm -[`CommandOptions.CommandNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm -[`CommandOptions.ParentCommand`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_ParentCommand.htm -[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm -[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandOptions.htm -[`CreateCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_AliasAttribute.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`AsyncCommandBase.CancellationToken`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_AsyncCommandBase_CancellationToken.htm +[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm +[`CancellationToken`]: https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken +[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandManager.GetCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm +[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm +[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`CommandNameTransform`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm +[`CommandOptions.AutoCommandPrefixAliases`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoCommandPrefixAliases.htm +[`CommandOptions.AutoVersionCommand`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand.htm +[`CommandOptions.CommandFilter`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter.htm +[`CommandOptions.CommandNameTransform`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm +[`CommandOptions.ParentCommand`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_ParentCommand.htm +[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm +[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandOptions.htm +[`CreateCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs -[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm -[`IAsyncCommand.RunAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm -[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm -[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[`ICommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommand.htm -[`ICommandWithCustomParsing.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse.htm -[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm -[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`NameTransform.DashCase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_NameTransform.htm -[`NameTransform.None`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_NameTransform.htm -[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Commands.htm -[`ParentCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommand.htm -[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm -[`ParseOptions.AutoVersionArgument`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_AutoVersionArgument.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm -[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm -[`ParseResult.Status`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_Status.htm -[`RunCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`RunCommand`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`IAsyncCancelableCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_IAsyncCancelableCommand.htm +[`IAsyncCommand.RunAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm +[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm +[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm +[`ICommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommand.htm +[`ICommandWithCustomParsing.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse.htm +[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm +[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`NameTransform.DashCase`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_NameTransform.htm +[`NameTransform.None`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_NameTransform.htm +[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-4.1/html/N_Ookii_CommandLine_Commands.htm +[`ParentCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ParentCommand.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`ParseOptions.AutoVersionArgument`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_AutoVersionArgument.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm +[`ParseResult.Status`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseResult_Status.htm +[`RunCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`RunCommand`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm [`StringComparison.OrdinalIgnoreCase`]: https://learn.microsoft.com/dotnet/api/system.stringcomparison -[`StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm -[`UsageWriter.IncludeCommandHelpInstruction`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[`WriteCommandDescription()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandDescription.htm -[`WriteCommandHelpInstruction()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandHelpInstruction.htm -[`WriteCommandListUsageCore()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageCore.htm -[`WriteCommandListUsageSyntax()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageSyntax.htm -[RunAsync()_0]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync.htm +[`StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm +[`UsageWriter.IncludeCommandHelpInstruction`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`WriteCommandDescription()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandDescription.htm +[`WriteCommandHelpInstruction()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandHelpInstruction.htm +[`WriteCommandListUsageCore()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageCore.htm +[`WriteCommandListUsageSyntax()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageSyntax.htm +[RunAsync()_0]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync.htm diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 8f1e26ab..d6effd52 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -12,7 +12,7 @@ Create a directory called "tutorial" for the project, and run the following comm directory: ```bash -dotnet new console --framework net7.0 +dotnet new console --framework net8.0 ``` Next, we will add a reference to Ookii.CommandLine's NuGet package: @@ -96,7 +96,7 @@ But wait, we didn't pass any arguments to this method? Actually, the method will explicit `string[]` array with the arguments, if you want to pass them manually. So, let's run our application. Build the application using `dotnet build`, and then, from the -`bin/Debug/net7.0` directory, run the following: +`bin/Debug/net8.0` directory, run the following: ```bash ./tutorial ../../../tutorial.csproj @@ -109,7 +109,7 @@ Which will give print the contents of the tutorial.csproj file: Exe - net7.0 + net8.0 enable enable @@ -974,52 +974,52 @@ following resources: - [Class library documentation](https://www.ookii.org/Link/CommandLineDoc) - [Sample applications](../src/Samples) -[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`Arguments.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParser_1_Parse.htm +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_AliasAttribute.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`Arguments.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_IParser_1_Parse.htm [`AssemblyTitleAttribute`]: https://learn.microsoft.com/dotnet/api/system.reflection.assemblytitleattribute -[`AsyncCommandBase.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm -[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm -[`CaseSensitive`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm -[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm -[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm -[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm -[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm -[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandOptions.htm -[`CreateCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm +[`AsyncCommandBase.Run()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm +[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm +[`CaseSensitive`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm +[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm +[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandOptions.htm +[`CreateCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs [`File.ReadLinesAsync()`]: https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync [`FileInfo`]: https://learn.microsoft.com/dotnet/api/system.io.fileinfo -[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm -[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm -[`GetCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm -[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`GetCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm +[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm [`IAsyncEnumerable`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.iasyncenumerable-1 -[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[`ICommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommand.htm -[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm +[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm +[`ICommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommand.htm +[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 -[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute.AutoPrefixAliases`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases.htm -[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm -[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParsingMode.htm -[`RunCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute.AutoPrefixAliases`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases.htm +[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ParsingMode.htm +[`RunCommand()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm [`StringComparison`]: https://learn.microsoft.com/dotnet/api/system.stringcomparison [`Take()`]: https://learn.microsoft.com/dotnet/api/system.linq.enumerable.take [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[IsPosix_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_IsPosix.htm -[Mode_2]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_Mode.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[Run()_0]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm -[Run()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[RunAsync()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm +[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[IsPosix_0]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_IsPosix.htm +[Mode_2]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_Mode.htm +[Parse()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[Run()_0]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm +[Run()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm +[RunAsync()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm diff --git a/docs/UsageHelp.md b/docs/UsageHelp.md index 4f6aa2fd..4bcd6b76 100644 --- a/docs/UsageHelp.md +++ b/docs/UsageHelp.md @@ -64,8 +64,8 @@ Usage: Parser [-Source] [-Destination] [[-OperationIndex] + First line of the description. + +And another line, after a blank line. + + -OtherArgument + Other description. +``` + +To avoid this, set the [`UsageWriter.IndentAfterEmptyLine`][] property to true. Now, the same usage +help will look like this: + +```text + -SomeArgument + First line of the description. + + And another line, after a blank line. + + -OtherArgument + Other description. +``` + ## Hidden arguments Sometimes, you may want an argument to be available, but not easily discoverable. For example, if @@ -252,6 +311,24 @@ public int Argument { get; set; } Note that positional and required arguments cannot be hidden. +## Usage help footer + +The application description is shown at the top of the usage help, but sometimes you may want to +add additional information at the bottom, for example to direct the user how to get additional +help. + +To add additional text below the argument descriptions, you can use the [`UsageFooterAttribute`][] +attribute. Apply this attribute to your command line arguments class to set a footer. + +```csharp +[GeneratedParser] +[Description("This is the application description.")] +[UsageFooter("For more information, see https://www.example.com")] +partial class Arguments +{ +} +``` + ## Color output When possible, Ookii.CommandLine will use color when writing usage help. This is controlled by the @@ -344,39 +421,46 @@ Please see the [subcommand documentation](Subcommands.md) for information about Next, we'll take a look at [argument validation and dependencies](Validation.md). -[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm -[`CommandLineParser.GetUsage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_GetUsage.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser.WriteUsage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_WriteUsage.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.DefaultValueFormat`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValueFormat.htm +[`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp.htm +[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm +[`CommandLineParser.GetUsage()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_GetUsage.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser.WriteUsage()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_WriteUsage.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute -[`DescriptionListFilterMode.Information`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_DescriptionListFilterMode.htm -[`GetExtendedColor()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_TextFormat_GetExtendedColor.htm +[`DescriptionListFilterMode.Information`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_DescriptionListFilterMode.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`GetExtendedColor()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Terminal_TextFormat_GetExtendedColor.htm [`Int32`]: https://learn.microsoft.com/dotnet/api/system.int32 -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 -[`ParseOptions.DefaultValueDescriptions`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions.htm -[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm -[`ParseOptions.UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_UsageWriter.htm +[`ParseOptions.DefaultValueDescriptions`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions.htm +[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm +[`ParseOptions.UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_UsageWriter.htm [`SetConsoleMode`]: https://learn.microsoft.com/windows/console/setconsolemode [`String`]: https://learn.microsoft.com/dotnet/api/system.string [`System.ComponentModel.DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute -[`TextFormat`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_TextFormat.htm -[`UsageWriter.ArgumentDescriptionListFilter`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListFilter.htm -[`UsageWriter.ArgumentDescriptionListOrder`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListOrder.htm -[`UsageWriter.IncludeApplicationDescription`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescription.htm -[`UsageWriter.UseAbbreviatedSyntax`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_UseAbbreviatedSyntax.htm -[`UsageWriter.UseColor`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_UseColor.htm -[`UsageWriter.UseShortNamesForSyntax`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_UseShortNamesForSyntax.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[`WriteArgumentDescriptions()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescriptions.htm -[`WriteArgumentName()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentName.htm -[`WriteArgumentSyntax()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentSyntax.htm -[`WriteParserUsageCore()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageCore.htm -[`WriteParserUsageSyntax()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageSyntax.htm -[`WriteValueDescription()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescription.htm -[`WriteValueDescriptionForDescription()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescriptionForDescription.htm -[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm -[WriteArgumentDescription()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescription.htm +[`TextFormat`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_TextFormat.htm +[`UsageFooterAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageFooterAttribute.htm +[`UsageWriter.ArgumentDescriptionListFilter`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListFilter.htm +[`UsageWriter.ArgumentDescriptionListOrder`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListOrder.htm +[`UsageWriter.IncludeApplicationDescription`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescription.htm +[`UsageWriter.IncludeDefaultValueInDescription`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeDefaultValueInDescription.htm +[`UsageWriter.IndentAfterEmptyLine`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IndentAfterEmptyLine.htm +[`UsageWriter.UseAbbreviatedSyntax`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_UseAbbreviatedSyntax.htm +[`UsageWriter.UseColor`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_UseColor.htm +[`UsageWriter.UseShortNamesForSyntax`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_UseShortNamesForSyntax.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[`WriteArgumentDescriptions()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescriptions.htm +[`WriteArgumentName()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentName.htm +[`WriteArgumentSyntax()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentSyntax.htm +[`WriteParserUsageCore()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageCore.htm +[`WriteParserUsageSyntax()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageSyntax.htm +[`WriteValueDescription()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescription.htm +[`WriteValueDescriptionForDescription()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescriptionForDescription.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[WriteArgumentDescription()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescription.htm diff --git a/docs/Utilities.md b/docs/Utilities.md index 31f069f1..665c2ca8 100644 --- a/docs/Utilities.md +++ b/docs/Utilities.md @@ -42,9 +42,10 @@ The [`LineWrappingTextWriter`][] class uses hanging indents, also called negativ lines except the first one are indented. The indentation level can be set using the [`LineWrappingTextWriter.Indent`][] property, which indicates the number of spaces to indent by. -When this property is set, it will apply to the next line that needs to be indented. The first line -of text, and any line after a blank line, is not indented. Indentation is applied both to lines that -were wrapped, and lines created by explicit new lines in the text. +When this property is set, it will apply to the next line that needs to be indented. Indentation is +applied both to lines that were wrapped, and lines created by explicit new lines in the text. The +first line of text is not indented. Lines after a blank line are not indented either, unless you set +the [`LineWrappingTextWriter.IndentAfterEmptyLine`][] property to true. You can change the [`Indent`][] property at any time to change the size of the indentation to use. @@ -128,6 +129,18 @@ and they return a disposable type that will revert the console mode when dispose collected. On other platforms, it only checks for support and disposing the returned instance does nothing. +To simplify writing messages to the console that use a single format for the whole message, two +helper methods are provided: [`VirtualTerminal.WriteLineFormatted()`][] and +[`VirtualTerminal.WriteLineErrorFormatted()`][]. These methods call [`EnableColor()`][], write the +message to either the standard output or standard error stream respectively, using the specified +formatting, and then reset the format to the default. + +The below example is identical to the one above: + +```csharp +VirtualTerminal.WriteLineFormatted("This text is green and underlined.", TextFormat.ForegroundGreen + TextFormat.Underline); +``` + In the [tutorial](Tutorial.md), we created an application with an `--inverted` argument, that actually just set the console to use a white background and a black foreground, instead of truly inverting the console colors. With virtual terminal support, we can update the `read` command to use @@ -163,20 +176,23 @@ public int Run() ``` [`Console.WindowWidth`]: https://learn.microsoft.com/dotnet/api/system.console.windowwidth -[`EnableColor()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableColor.htm -[`EnableVirtualTerminalSequences()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableVirtualTerminalSequences.htm -[`Indent`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm -[`LineWrappingTextWriter.ForConsoleError()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError.htm -[`LineWrappingTextWriter.ForConsoleOut()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut.htm -[`LineWrappingTextWriter.Indent`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm -[`LineWrappingTextWriter.ResetIndent()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm -[`Ookii.CommandLine.Terminal`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Terminal.htm -[`ResetIndent()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm -[`TextFormat`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_TextFormat.htm +[`EnableColor()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableColor.htm +[`EnableVirtualTerminalSequences()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableVirtualTerminalSequences.htm +[`Indent`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm +[`LineWrappingTextWriter.ForConsoleError()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError.htm +[`LineWrappingTextWriter.ForConsoleOut()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut.htm +[`LineWrappingTextWriter.Indent`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm +[`LineWrappingTextWriter.IndentAfterEmptyLine`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_IndentAfterEmptyLine.htm +[`LineWrappingTextWriter.ResetIndent()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm +[`LineWrappingTextWriter.Wrapping`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm +[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`Ookii.CommandLine.Terminal`]: https://www.ookii.org/docs/commandline-4.1/html/N_Ookii_CommandLine_Terminal.htm +[`ResetIndent()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm +[`TextFormat`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_TextFormat.htm [`TextWriter`]: https://learn.microsoft.com/dotnet/api/system.io.textwriter -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[`VirtualTerminal`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_VirtualTerminal.htm -[`LineWrappingTextWriter.Wrapping`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm -[`WrappingMode.Disabled`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_WrappingMode.htm -[`WrappingMode.EnabledNoForce`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_WrappingMode.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`VirtualTerminal.WriteLineErrorFormatted()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_WriteLineErrorFormatted.htm +[`VirtualTerminal.WriteLineFormatted()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_WriteLineFormatted.htm +[`VirtualTerminal`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Terminal_VirtualTerminal.htm +[`WrappingMode.Disabled`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_WrappingMode.htm +[`WrappingMode.EnabledNoForce`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_WrappingMode.htm diff --git a/docs/Validation.md b/docs/Validation.md index 52af1025..bb06654b 100644 --- a/docs/Validation.md +++ b/docs/Validation.md @@ -22,16 +22,16 @@ There are validators that check the value of an argument, and validators that ch inter-dependencies. The following are the built-in argument value validators (dependency validators are discussed [below](#argument-dependencies-and-restrictions)): -Validator | Description | Applied --------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------- -[`ValidateCountAttribute`][] | Validates that the number of items for a multi-value argument is in the specified range. | After parsing. -[`ValidateEnumValueAttribute`][] | Validates that the value is one of the defined values for an enumeration. The [`EnumConverter`][] class allows conversion from the underlying value, even if that value is not a defined value for the enumeration. This validator prevents that. See also [enumeration type conversion](Arguments.md#enumeration-conversion). | After conversion. -[`ValidateNotEmptyAttribute`][] | Validates that the value of an argument is not an empty string. | Before conversion. -[`ValidateNotNullAttribute`][] | Validates that the value of an argument is not null. This is only useful if the [`ArgumentConverter`][] for an argument can return null (for example, the [`NullableConverter`][] can). It's not necessary to use this validator on non-nullable value types, or if using .Net 6.0 or later, or [source generation](SourceGeneration.md), on non-nullable reference types. | After conversion. -[`ValidateNotWhiteSpaceAttribute`][] | Validates that the value of an argument is not an empty string or a string containing only white-space characters. | Before conversion. -[`ValidatePatternAttribute`][] | Validates that the value of an argument matches the specified regular expression. | Before conversion. -[`ValidateRangeAttribute`][] | Validates that the value of an argument is in the specified range. This can be used on any type that implements the [`IComparable`][] interface. | After conversion. -[`ValidateStringLengthAttribute`][] | Validates that the length of an argument's string value is in the specified range. | Before conversion. +Validator | Description | Applied +-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------- +[`ValidateCountAttribute`][] | Validates that the number of items for a multi-value argument is in the specified range. | After parsing. +[`ValidateEnumValueAttribute`][] | Validates that the value is one of the defined values for an enumeration. The [`EnumConverter`][] class allows conversion from the underlying value, even if that value is not a defined value for the enumeration. This validator prevents that. It can also be used to customize the behavior of the [`EnumConverter`][] class. See also [enumeration type conversion](Arguments.md#enumeration-conversion). | After conversion. +[`ValidateNotEmptyAttribute`][] | Validates that the value of an argument is not an empty string. | Before conversion. +[`ValidateNotNullAttribute`][] | Validates that the value of an argument is not null. This is only useful if the [`ArgumentConverter`][] for an argument can return null (for example, the [`NullableConverter`][] can). It's not necessary to use this validator on non-nullable value types, or if using .Net 6.0 or later, or [source generation](SourceGeneration.md), on non-nullable reference types. | After conversion. +[`ValidateNotWhiteSpaceAttribute`][] | Validates that the value of an argument is not an empty string or a string containing only white-space characters. | Before conversion. +[`ValidatePatternAttribute`][] | Validates that the value of an argument matches the specified regular expression. | Before conversion. +[`ValidateRangeAttribute`][] | Validates that the value of an argument is in the specified range. This can be used on any type that implements the [`IComparable`][] interface. | After conversion. +[`ValidateStringLengthAttribute`][] | Validates that the length of an argument's string value is in the specified range. | Before conversion. Note that there is no `ValidateSetAttribute`, or an equivalent way to make sure that an argument is one of a predefined set of values, because you're encouraged to use an enumeration type for this @@ -280,45 +280,45 @@ does not apply to validators that don't use [`ValidationMode.BeforeConversion`][ Now that you know (almost) everything there is to know about arguments, let's move on to [subcommands](Subcommands.md). -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`ArgumentValidationAttribute.IsSpanValid`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsSpanValid.htm -[`ArgumentValidationAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ArgumentValidationAttribute.htm -[`ArgumentValidationWithHelpAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute.htm -[`Category`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm -[`ClassValidationAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ClassValidationAttribute.htm -[`CommandLineArgumentErrorCategory.ValidationFailed`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm -[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentException.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentValidationAttribute.IsSpanValid`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsSpanValid.htm +[`ArgumentValidationAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ArgumentValidationAttribute.htm +[`ArgumentValidationWithHelpAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute.htm +[`Category`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm +[`ClassValidationAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ClassValidationAttribute.htm +[`CommandLineArgumentErrorCategory.ValidationFailed`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentException.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm [`DateOnly`]: https://learn.microsoft.com/dotnet/api/system.dateonly -[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm -[`ErrorCategory`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory.htm -[`GetErrorMessage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage.htm -[`GetUsageHelp()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetUsageHelp.htm -[`GetUsageHelpCore()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_GetUsageHelpCore.htm +[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm +[`ErrorCategory`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory.htm +[`GetErrorMessage()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage.htm +[`GetUsageHelp()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetUsageHelp.htm +[`GetUsageHelpCore()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_GetUsageHelpCore.htm [`IComparable`]: https://learn.microsoft.com/dotnet/api/system.icomparable-1 -[`IsValid()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`NullableConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_NullableConverter.htm -[`Ookii.CommandLine.Validation`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Validation.htm -[`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm +[`IsValid()`]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`NullableConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_NullableConverter.htm +[`Ookii.CommandLine.Validation`]: https://www.ookii.org/docs/commandline-4.1/html/N_Ookii_CommandLine_Validation.htm +[`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm [`ReadOnlySpan`]: https://learn.microsoft.com/dotnet/api/system.readonlyspan-1 -[`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm -[`RequiresAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm -[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm -[`ValidateCountAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateCountAttribute.htm -[`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage.htm -[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm -[`ValidateNotEmptyAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateNotEmptyAttribute.htm -[`ValidateNotNullAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateNotNullAttribute.htm -[`ValidateNotWhiteSpaceAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateNotWhiteSpaceAttribute.htm -[`ValidatePatternAttribute.ErrorMessage`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ValidatePatternAttribute_ErrorMessage.htm -[`ValidatePatternAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidatePatternAttribute.htm -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[`ValidateStringLengthAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateStringLengthAttribute.htm -[`ValidationMode.BeforeConversion`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidationMode.htm -[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm -[Mode_3]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_Mode.htm -[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm -[ValidationFailed_1]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm +[`RequiresAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm +[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm +[`ValidateCountAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateCountAttribute.htm +[`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage.htm +[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm +[`ValidateNotEmptyAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateNotEmptyAttribute.htm +[`ValidateNotNullAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateNotNullAttribute.htm +[`ValidateNotWhiteSpaceAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateNotWhiteSpaceAttribute.htm +[`ValidatePatternAttribute.ErrorMessage`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ValidatePatternAttribute_ErrorMessage.htm +[`ValidatePatternAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidatePatternAttribute.htm +[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm +[`ValidateStringLengthAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateStringLengthAttribute.htm +[`ValidationMode.BeforeConversion`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidationMode.htm +[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm +[Mode_3]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_Mode.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[ValidationFailed_1]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm diff --git a/docs/refs.json b/docs/refs.json index 46095986..a267ef1f 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -1,11 +1,13 @@ { "#apiPrefix": "https://learn.microsoft.com/dotnet/api/", - "#prefix": "https://www.ookii.org/docs/commandline-4.0/html/", + "#prefix": "https://www.ookii.org/docs/commandline-4.1/html/", "#suffix": ".htm", "AddCommand": null, "AliasAttribute": "T_Ookii_CommandLine_AliasAttribute", + "AllowCommaSeparatedValues": "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowCommaSeparatedValues", "AllowDuplicateDictionaryKeys": "P_Ookii_CommandLine_CommandLineArgument_AllowDuplicateDictionaryKeys", "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", + "AllowNumericValues": "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowNumericValues", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", "Arg1": null, "Arg2": null, @@ -29,8 +31,10 @@ "ArgumentValidationWithHelpAttribute": "T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute", "AssemblyTitleAttribute": "#system.reflection.assemblytitleattribute", "AsyncCommandBase": "T_Ookii_CommandLine_Commands_AsyncCommandBase", + "AsyncCommandBase.CancellationToken": "P_Ookii_CommandLine_Commands_AsyncCommandBase_CancellationToken", "AsyncCommandBase.Run()": "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", "Bar": null, + "CancellationToken": "#system.threading.cancellationtoken", "CancelMode": "T_Ookii_CommandLine_CancelMode", "CancelMode.Abort": "T_Ookii_CommandLine_CancelMode", "CancelMode.None": "T_Ookii_CommandLine_CancelMode", @@ -39,7 +43,10 @@ "P_Ookii_CommandLine_CommandLineArgument_CancelParsing", "P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing" ], - "CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", + "CaseSensitive": [ + "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", + "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_CaseSensitive" + ], "Category": "P_Ookii_CommandLine_CommandLineArgumentException_Category", "ClassValidationAttribute": "T_Ookii_CommandLine_Validation_ClassValidationAttribute", "CommandAttribute": "T_Ookii_CommandLine_Commands_CommandAttribute", @@ -54,6 +61,8 @@ "CommandLineArgumentAttribute": "T_Ookii_CommandLine_CommandLineArgumentAttribute", "CommandLineArgumentAttribute.CancelParsing": "P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing", "CommandLineArgumentAttribute.DefaultValue": "P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue", + "CommandLineArgumentAttribute.DefaultValueFormat": "P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValueFormat", + "CommandLineArgumentAttribute.IncludeDefaultInUsageHelp": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp", "CommandLineArgumentAttribute.IsHidden": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden", "CommandLineArgumentAttribute.IsLong": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong", "CommandLineArgumentAttribute.IsPositional": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional", @@ -76,6 +85,7 @@ "CommandLineParser.Parse()": "M_Ookii_CommandLine_CommandLineParser_Parse__1", "CommandLineParser.ParseResult": "P_Ookii_CommandLine_CommandLineParser_ParseResult", "CommandLineParser.ParseWithErrorHandling()": "Overload_Ookii_CommandLine_CommandLineParser_ParseWithErrorHandling", + "CommandLineParser.UnknownArgument": "E_Ookii_CommandLine_CommandLineParser_UnknownArgument", "CommandLineParser.WriteUsage()": "M_Ookii_CommandLine_CommandLineParser_WriteUsage", "CommandLineParser": "T_Ookii_CommandLine_CommandLineParser_1", "CommandLineParser.Parse()": "Overload_Ookii_CommandLine_CommandLineParser_1_Parse", @@ -158,6 +168,7 @@ "GetUsageHelp()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetUsageHelp", "GetUsageHelpCore()": "M_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_GetUsageHelpCore", "HelpRequested": "P_Ookii_CommandLine_CommandLineParser_HelpRequested", + "IAsyncCancelableCommand": "T_Ookii_CommandLine_Commands_IAsyncCancelableCommand", "IAsyncCommand": "T_Ookii_CommandLine_Commands_IAsyncCommand", "IAsyncCommand.RunAsync()": "M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync", "IAsyncEnumerable": "#system.collections.generic.iasyncenumerable-1", @@ -209,6 +220,7 @@ "LineWrappingTextWriter.ForConsoleError()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError", "LineWrappingTextWriter.ForConsoleOut()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut", "LineWrappingTextWriter.Indent": "P_Ookii_CommandLine_LineWrappingTextWriter_Indent", + "LineWrappingTextWriter.IndentAfterEmptyLine": "P_Ookii_CommandLine_LineWrappingTextWriter_IndentAfterEmptyLine", "LineWrappingTextWriter.ResetIndent()": "M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent", "LineWrappingTextWriter.ToString()": "M_Ookii_CommandLine_LineWrappingTextWriter_ToString", "LineWrappingTextWriter.Wrapping": "P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping", @@ -273,6 +285,7 @@ "ParseOptions.IsPosix": "P_Ookii_CommandLine_ParseOptions_IsPosix", "ParseOptions.Mode": "P_Ookii_CommandLine_ParseOptions_Mode", "ParseOptions.NameValueSeparators": "P_Ookii_CommandLine_ParseOptions_NameValueSeparators", + "ParseOptions.PrefixTermination": "P_Ookii_CommandLine_ParseOptions_PrefixTermination", "ParseOptions.ShowUsageOnError": "P_Ookii_CommandLine_ParseOptions_ShowUsageOnError", "ParseOptions.StringProvider": "P_Ookii_CommandLine_ParseOptions_StringProvider", "ParseOptions.UsageWriter": "P_Ookii_CommandLine_ParseOptions_UsageWriter", @@ -283,6 +296,7 @@ "ParseOptionsAttribute.CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", "ParseOptionsAttribute.IsPosix": "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix", "ParseOptionsAttribute.NameValueSeparators": "P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators", + "ParseOptionsAttribute.PrefixTermination": "P_Ookii_CommandLine_ParseOptionsAttribute_PrefixTermination", "ParseResult.ArgumentName": "P_Ookii_CommandLine_ParseResult_ArgumentName", "ParseResult.LastException": "P_Ookii_CommandLine_ParseResult_LastException", "ParseResult.RemainingArguments": "P_Ookii_CommandLine_ParseResult_RemainingArguments", @@ -303,6 +317,7 @@ "P_Ookii_CommandLine_CommandLineArgument_Position", "P_Ookii_CommandLine_CommandLineArgumentAttribute_Position" ], + "PrefixTerminationMode.CancelWithSuccess": "T_Ookii_CommandLine_PrefixTerminationMode", "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", "ReadCommand": null, "ReadDirectoryCommand": null, @@ -334,6 +349,8 @@ ], "SomeName": null, "SortedDictionary": "#system.collections.generic.sorteddictionary-2", + "StandardStream": "T_Ookii_CommandLine_Terminal_StandardStream", + "StandardStreamExtensions": "T_Ookii_CommandLine_Terminal_StandardStreamExtensions", "StreamReader": "#system.io.streamreader", "String": "#system.string", "StringComparison": "#system.stringcomparison", @@ -349,19 +366,26 @@ "TypeConverter": "#system.componentmodel.typeconverter", "TypeConverterAttribute": "#system.componentmodel.typeconverterattribute", "TypeDescriptor.GetConverter()": "#system.componentmodel.typedescriptor.getconverter", + "UnknownArgument": "E_Ookii_CommandLine_CommandLineParser_UnknownArgument", "Uri": "#system.uri", + "UsageFooterAttribute": "T_Ookii_CommandLine_UsageFooterAttribute", "UsageHelpRequest.SyntaxOnly": "T_Ookii_CommandLine_UsageHelpRequest", "UsageWriter": "T_Ookii_CommandLine_UsageWriter", "UsageWriter.ArgumentDescriptionListFilter": "P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListFilter", "UsageWriter.ArgumentDescriptionListOrder": "P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListOrder", "UsageWriter.IncludeApplicationDescription": "P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescription", "UsageWriter.IncludeCommandHelpInstruction": "P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction", + "UsageWriter.IncludeDefaultValueInDescription": "P_Ookii_CommandLine_UsageWriter_IncludeDefaultValueInDescription", "UsageWriter.IncludeValidatorsInDescription": "P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription", + "UsageWriter.IndentAfterEmptyLine": "P_Ookii_CommandLine_UsageWriter_IndentAfterEmptyLine", "UsageWriter.UseAbbreviatedSyntax": "P_Ookii_CommandLine_UsageWriter_UseAbbreviatedSyntax", "UsageWriter.UseColor": "P_Ookii_CommandLine_UsageWriter_UseColor", "UsageWriter.UseShortNamesForSyntax": "P_Ookii_CommandLine_UsageWriter_UseShortNamesForSyntax", "ValidateCountAttribute": "T_Ookii_CommandLine_Validation_ValidateCountAttribute", "ValidateEnumValueAttribute": "T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute", + "ValidateEnumValueAttribute.AllowCommaSeparatedValues": "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowCommaSeparatedValues", + "ValidateEnumValueAttribute.AllowNumericValues": "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_AllowNumericValues", + "ValidateEnumValueAttribute.CaseSensitive": "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_CaseSensitive", "ValidateEnumValueAttribute.IncludeValuesInErrorMessage": "P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage", "ValidateNotEmptyAttribute": "T_Ookii_CommandLine_Validation_ValidateNotEmptyAttribute", "ValidateNotNullAttribute": "T_Ookii_CommandLine_Validation_ValidateNotNullAttribute", @@ -378,6 +402,8 @@ "ValueConverterAttribute": "T_Ookii_CommandLine_Conversion_ValueConverterAttribute", "ValueDescriptionAttribute": "T_Ookii_CommandLine_ValueDescriptionAttribute", "VirtualTerminal": "T_Ookii_CommandLine_Terminal_VirtualTerminal", + "VirtualTerminal.WriteLineErrorFormatted()": "M_Ookii_CommandLine_Terminal_VirtualTerminal_WriteLineErrorFormatted", + "VirtualTerminal.WriteLineFormatted()": "M_Ookii_CommandLine_Terminal_VirtualTerminal_WriteLineFormatted", "Wrapping": "P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping", "WrappingMode.Disabled": "T_Ookii_CommandLine_WrappingMode", "WrappingMode.EnabledNoForce": "T_Ookii_CommandLine_WrappingMode", diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 88fe7c83..3e5bff02 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,6 @@ Sven Groot Ookii.org Copyright (c) Sven Groot (Ookii.org) - 4.0.1 + 4.1.0 \ No newline at end of file diff --git a/src/Ookii.CommandLine.Generator/AnalyzerReleases.Shipped.md b/src/Ookii.CommandLine.Generator/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..f38cffc1 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/AnalyzerReleases.Shipped.md @@ -0,0 +1,10 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 4.1 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +OCL0040 | Ookii.CommandLine | Warning | ParserShouldBeGenerated diff --git a/src/Ookii.CommandLine.Generator/AnalyzerReleases.Unshipped.md b/src/Ookii.CommandLine.Generator/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..6ed4fa04 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,4 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +; This is only for rules used by the analyzer; rules for the source generator should not be listed +; here. diff --git a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs index d9e5096f..7b080f45 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs @@ -14,6 +14,7 @@ internal class ArgumentAttributes private readonly AttributeData? _converterAttribute; private readonly AttributeData? _keyConverterAttribute; private readonly AttributeData? _valueConverterAttribute; + private readonly AttributeData? _validateEnumValue; private readonly List? _aliases; private readonly List? _shortAliases; private readonly List? _validators; @@ -23,7 +24,7 @@ public ArgumentAttributes(ISymbol member, TypeHelper typeHelper, SourceProductio AttributeData? typeConverterAttribute = null; foreach (var attribute in member.GetAttributes()) { - var _ = attribute.CheckType(typeHelper.CommandLineArgumentAttribute, ref _commandLineArgumentAttribute) || + _ = attribute.CheckType(typeHelper.CommandLineArgumentAttribute, ref _commandLineArgumentAttribute) || attribute.CheckType(typeHelper.MultiValueSeparatorAttribute, ref _multiValueSeparator) || attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || attribute.CheckType(typeHelper.ValueDescriptionAttribute, ref _valueDescription) || @@ -34,11 +35,19 @@ public ArgumentAttributes(ISymbol member, TypeHelper typeHelper, SourceProductio attribute.CheckType(typeHelper.ValueConverterAttribute, ref _valueConverterAttribute) || attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || attribute.CheckType(typeHelper.ShortAliasAttribute, ref _shortAliases) || - attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators); + attribute.CheckType(typeHelper.ValidateEnumValueAttribute, ref _validateEnumValue) || + attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators) || attribute.CheckType(typeHelper.TypeConverterAttribute, ref typeConverterAttribute); } - // Only warn if the TypeConverterAttribute is present. + // Since it was checked for separately, it won't be in the list. + if (_validateEnumValue != null) + { + _validators ??= []; + _validators.Add(_validateEnumValue); + } + + // Warn if the TypeConverterAttribute is present. if (CommandLineArgument != null && typeConverterAttribute != null) { context.ReportDiagnostic(Diagnostics.IgnoredTypeConverterAttribute(member, typeConverterAttribute)); @@ -57,5 +66,6 @@ public ArgumentAttributes(ISymbol member, TypeHelper typeHelper, SourceProductio public List? Aliases => _aliases; public List? ShortAliases => _shortAliases; public List? Validators => _validators; + public AttributeData? ValidateEnumValue => _validateEnumValue; } diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index 73519641..2847dc87 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -6,6 +6,7 @@ internal readonly struct ArgumentsClassAttributes { private readonly AttributeData? _parseOptions; private readonly AttributeData? _description; + private readonly AttributeData? _usageFooter; private readonly AttributeData? _applicationFriendlyName; private readonly AttributeData? _command; private readonly AttributeData? _generatedParser; @@ -22,6 +23,7 @@ public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper) { var _ = attribute.CheckType(typeHelper.ParseOptionsAttribute, ref _parseOptions) || attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || + attribute.CheckType(typeHelper.UsageFooterAttribute, ref _usageFooter) || attribute.CheckType(typeHelper.ApplicationFriendlyNameAttribute, ref _applicationFriendlyName) || attribute.CheckType(typeHelper.CommandAttribute, ref _command) || attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || @@ -34,6 +36,7 @@ public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper) public AttributeData? ParseOptions => _parseOptions; public AttributeData? Description => _description; + public AttributeData? UsageFooter => _usageFooter; public AttributeData? ApplicationFriendlyName => _applicationFriendlyName; public AttributeData? Command => _command; public AttributeData? GeneratedParser => _generatedParser; diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index b454c5b5..182ee91e 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -100,6 +100,7 @@ public void Generate() var builder = new SourceBuilder(manager.ContainingNamespace); builder.AppendLine($"partial class {manager.Name} : Ookii.CommandLine.Commands.CommandManager"); builder.OpenBlock(); + builder.AppendGeneratedCodeAttribute(); builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.CommandProvider"); builder.OpenBlock(); builder.AppendLine("public override Ookii.CommandLine.Support.ProviderKind Kind => Ookii.CommandLine.Support.ProviderKind.Generated;"); @@ -156,6 +157,7 @@ public void Generate() builder.CloseBlock(); // GetCommandsUnsorted builder.CloseBlock(); // provider class builder.AppendLine(); + builder.AppendGeneratedCodeAttribute(); builder.AppendLine($"public {manager.Name}(Ookii.CommandLine.Commands.CommandOptions? options = null)"); builder.AppendLine($" : base(new GeneratedProvider(), options)"); builder.OpenBlock(); diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 5852e956..a62fe5b8 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -184,6 +184,7 @@ private static string GenerateName(ITypeSymbol type) private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, ConverterInfo info) { + builder.AppendGeneratedCodeAttribute(); builder.AppendLine($"internal class {info.Name} : Ookii.CommandLine.Conversion.ArgumentConverter"); builder.OpenBlock(); string inputType = info.UseSpan ? "System.ReadOnlySpan" : "string"; diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index dae08fc0..c26a2db5 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -366,15 +366,47 @@ public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location l location, symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + public static readonly DiagnosticDescriptor ParserShouldBeGeneratedDescriptor = CreateDiagnosticDescriptor( + "OCL0040", + nameof(Resources.ParserShouldBeGeneratedTitle), + nameof(Resources.ParserShouldBeGeneratedMessageFormat), + DiagnosticSeverity.Warning); + + public static Diagnostic ParserShouldBeGenerated(ISymbol symbol) + => Diagnostic.Create( + ParserShouldBeGeneratedDescriptor, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + + public static Diagnostic ValidateEnumInvalidType(ISymbol symbol, ITypeSymbol elementType) => CreateDiagnostic( + "OCL0041", + nameof(Resources.ValidateEnumInvalidTypeTitle), + nameof(Resources.ValidateEnumInvalidTypeMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + elementType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + + public static Diagnostic ValidateEnumWithCustomConverter(ISymbol symbol) => CreateDiagnostic( + "OCL0042", + nameof(Resources.ValidateEnumWithCustomConverterTitle), + nameof(Resources.ValidateEnumWithCustomConverterMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( - new DiagnosticDescriptor( - id, - new LocalizableResourceString(titleResource, Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(messageResource, Resources.ResourceManager, typeof(Resources)), - Category, - severity, - isEnabledByDefault: true, - helpLinkUri: $"https://www.ookii.org/Link/CommandLineGeneratorError#{id.ToLowerInvariant()}"), + CreateDiagnosticDescriptor(id, titleResource, messageResource, severity), location, messageArgs); + + private static DiagnosticDescriptor CreateDiagnosticDescriptor(string id, string titleResource, string messageResource, DiagnosticSeverity severity) + => new( + id, + new LocalizableResourceString(titleResource, Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(messageResource, Resources.ResourceManager, typeof(Resources)), + Category, + severity, + isEnabledByDefault: true, + helpLinkUri: $"https://www.ookii.org/Link/CommandLineGeneratorError#{id.ToLowerInvariant()}"); } diff --git a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj index 2df9996d..e096e179 100644 --- a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj +++ b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj @@ -3,7 +3,7 @@ netstandard2.0 false - 11.0 + 12.0 enable enable true @@ -17,8 +17,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/src/Ookii.CommandLine.Generator/ParserAnalyzer.cs b/src/Ookii.CommandLine.Generator/ParserAnalyzer.cs new file mode 100644 index 00000000..e94c8907 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ParserAnalyzer.cs @@ -0,0 +1,64 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace Ookii.CommandLine.Generator; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ParserAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Diagnostics.ParserShouldBeGeneratedDescriptor); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + } + + private void AnalyzeNamedType(SymbolAnalysisContext context) + { + var symbol = (INamedTypeSymbol)context.Symbol; + var tree = symbol.Locations[0].SourceTree; + var languageVersion = (tree?.Options as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.CSharp1; + if (languageVersion < LanguageVersion.CSharp8 || + symbol.IsAbstract || + symbol.ContainingType != null || + symbol.IsGenericType || + !symbol.IsReferenceType) + { + // Unsupported. + return; + } + + var typeHelper = new TypeHelper(context.Compilation); + if (typeHelper.GeneratedParserAttribute == null || typeHelper.CommandLineArgumentAttribute == null) + { + // Required types don't exist somehow. + return; + } + + if (symbol.GetAttribute(typeHelper.GeneratedParserAttribute) != null) + { + // Class is already using the attribute. + return; + } + + var argumentAttribute = typeHelper.CommandLineArgumentAttribute; + foreach (var member in symbol.GetMembers()) + { + if (member.DeclaredAccessibility == Accessibility.Public && + member.Kind is SymbolKind.Property or SymbolKind.Method) + { + if (member.GetAttribute(argumentAttribute) != null) + { + // Found a member with the CommandLineArgumentAttribute on a type that doesn't + // have the GeneratedParserAttribute. + context.ReportDiagnostic(Diagnostics.ParserShouldBeGenerated(symbol)); + break; + } + } + } + } +} diff --git a/src/Ookii.CommandLine.Generator/ParserCodeFixProvider.cs b/src/Ookii.CommandLine.Generator/ParserCodeFixProvider.cs new file mode 100644 index 00000000..ded0b5e6 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ParserCodeFixProvider.cs @@ -0,0 +1,74 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using System.Collections.Immutable; +using System.Composition; + +namespace Ookii.CommandLine.Generator; +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ParserCodeFixProvider)), Shared] +public class ParserCodeFixProvider : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(Diagnostics.ParserShouldBeGeneratedDescriptor.Id); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + var span = diagnostic.Location.SourceSpan; + + // Find the type declaration. + var declaration = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType().FirstOrDefault(); + if (declaration == null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + Properties.Resources.GeneratedParserCodeFixTitle, + (token) => AddGeneratedParserAttribute(context.Document, declaration, token), + nameof(Properties.Resources.GeneratedParserCodeFixTitle)), + diagnostic); + } + + private static async Task AddGeneratedParserAttribute(Document document, TypeDeclarationSyntax declaration, + CancellationToken token) + { + var attr = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("GeneratedParser")); + var attrList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr)); + + // Add the attribute. + var newDeclaration = declaration.AddAttributeLists(attrList); + + // Add partial keyword if not already there. + if (!newDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + newDeclaration = newDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + } + + newDeclaration = newDeclaration.WithAdditionalAnnotations(Formatter.Annotation); + if (await document.GetSyntaxRootAsync(token).ConfigureAwait(false) is not CompilationUnitSyntax oldRoot) + { + return document; + } + + var newRoot = oldRoot.ReplaceNode(declaration, newDeclaration); + + // Add a using statement if needed. + if (!oldRoot.Usings.Any(u => u.Name.ToString() == "Ookii.CommandLine")) + { + newRoot = newRoot.AddUsings( + SyntaxFactory.UsingDirective( + SyntaxFactory.QualifiedName( + SyntaxFactory.IdentifierName("Ookii"), + SyntaxFactory.IdentifierName("CommandLine")))); + } + + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 3611f4d9..72c300f9 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -165,6 +165,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder.AppendLine("/// "); _builder.AppendLine($"/// An instance of the class for the class."); _builder.AppendLine("/// "); + _builder.AppendGeneratedCodeAttribute(); _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.ToQualifiedName()}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new Ookii.CommandLine.CommandLineParser<{_argumentsClass.ToQualifiedName()}>(new OokiiCommandLineArgumentProvider(), options);"); _builder.AppendLine(); var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); @@ -185,6 +186,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder.AppendLine($"/// An instance of the class, or if an"); _builder.AppendLine("/// error occurred or argument parsing was canceled."); _builder.AppendLine("/// "); + _builder.AppendGeneratedCodeAttribute(); _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); _builder.AppendLine(); _builder.AppendLine("/// "); @@ -199,6 +201,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder.AppendLine($"/// An instance of the class, or if an"); _builder.AppendLine("/// error occurred or argument parsing was canceled."); _builder.AppendLine("/// "); + _builder.AppendGeneratedCodeAttribute(); _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); _builder.AppendLine(); _builder.AppendLine("/// "); @@ -213,6 +216,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder.AppendLine($"/// An instance of the class, or if an"); _builder.AppendLine("/// error occurred or argument parsing was canceled."); _builder.AppendLine("/// "); + _builder.AppendGeneratedCodeAttribute(); _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(System.ReadOnlyMemory args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); _builder.CloseBlock(); // class } @@ -222,8 +226,15 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isCommand) { + _builder.AppendGeneratedCodeAttribute(); _builder.AppendLine("private class OokiiCommandLineArgumentProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); _builder.OpenBlock(); + if (attributes.UsageFooter != null) + { + _builder.AppendLine($"private readonly Ookii.CommandLine.UsageFooterAttribute _usageFooter = {attributes.UsageFooter.CreateInstantiation()};"); + _builder.AppendLine(); + } + _builder.AppendLine("public OokiiCommandLineArgumentProvider()"); _builder.IncreaseIndent(); _builder.AppendLine(": base("); @@ -239,6 +250,12 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman _builder.AppendLine(); _builder.AppendLine($"public override bool IsCommand => {isCommand.ToCSharpString()};"); _builder.AppendLine(); + if (attributes.UsageFooter != null) + { + _builder.AppendLine("public override string UsageFooter => _usageFooter.Footer;"); + _builder.AppendLine(); + } + _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); _builder.OpenBlock(); @@ -697,6 +714,21 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> _context.ReportDiagnostic(Diagnostics.IsShortIgnored(member, attributes.CommandLineArgument)); } + if (attributes.ValidateEnumValue != null) + { + if (elementType.TypeKind != TypeKind.Enum) + { + _context.ReportDiagnostic(Diagnostics.ValidateEnumInvalidType(member, elementType)); + } + else if (attributes.Converter != null && + (attributes.ValidateEnumValue.GetNamedArgument("CaseSensitive") != null || + attributes.ValidateEnumValue.GetNamedArgument("AllowCommaSeparatedValues") != null || + attributes.ValidateEnumValue.GetNamedArgument("AllowNumericValues") != null)) + { + _context.ReportDiagnostic(Diagnostics.ValidateEnumWithCustomConverter(member)); + } + } + return true; } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 7b474f6a..c459f09b 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -312,6 +312,15 @@ internal static string GeneratedCustomParsingCommandTitle { } } + /// + /// Looks up a localized string similar to Add GeneratedParserAttribute. + /// + internal static string GeneratedParserCodeFixTitle { + get { + return ResourceManager.GetString("GeneratedParserCodeFixTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The {0} attribute is ignored for the dictionary argument defined by {1} that has the ArgumentConverterAttribute attribute.. /// @@ -672,6 +681,24 @@ internal static string ParentCommandStringNotSupportedTitle { } } + /// + /// Looks up a localized string similar to The command line arguments class '{0}' should use the GeneratedParserAttribute.. + /// + internal static string ParserShouldBeGeneratedMessageFormat { + get { + return ResourceManager.GetString("ParserShouldBeGeneratedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line arguments class should use the GeneratedParserAttribute.. + /// + internal static string ParserShouldBeGeneratedTitle { + get { + return ResourceManager.GetString("ParserShouldBeGeneratedTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The positional argument defined by {0} comes after {1}, which is a multi-value argument and must come last.. /// @@ -815,5 +842,41 @@ internal static string UnsupportedLanguageVersionTitle { return ResourceManager.GetString("UnsupportedLanguageVersionTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to The argument defined by '{0}' uses the ValidateEnumValueAttribute, but its type '{1}' is not an enumeration.. + /// + internal static string ValidateEnumInvalidTypeMessageFormat { + get { + return ResourceManager.GetString("ValidateEnumInvalidTypeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ValidateEnumValueAttribute can only be used on arguments with an enum type.. + /// + internal static string ValidateEnumInvalidTypeTitle { + get { + return ResourceManager.GetString("ValidateEnumInvalidTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument defined by '{0}' has the ArgumentConverterAttribute, and uses ValidateEnumValueAttribute properties that may not work with a custom ArgumentConverter.. + /// + internal static string ValidateEnumWithCustomConverterMessageFormat { + get { + return ResourceManager.GetString("ValidateEnumWithCustomConverterMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument uses ValidateEnumValueAttribute properties that may not work with a custom ArgumentConverter.. + /// + internal static string ValidateEnumWithCustomConverterTitle { + get { + return ResourceManager.GetString("ValidateEnumWithCustomConverterTitle", resourceCulture); + } + } } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 40bd3799..bf739897 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -201,6 +201,9 @@ The GeneratedParserAttribute cannot be used with a class that implements the ICommandWithCustomParsing interface. + + Add GeneratedParserAttribute + The {0} attribute is ignored for the dictionary argument defined by {1} that has the ArgumentConverterAttribute attribute. @@ -321,6 +324,12 @@ The ParentCommandAttribute must use the typeof keyword. + + The command line arguments class '{0}' should use the GeneratedParserAttribute. + + + The command line arguments class should use the GeneratedParserAttribute. + The positional argument defined by {0} comes after {1}, which is a multi-value argument and must come last. @@ -369,4 +378,16 @@ Ookii.CommandLine source generation requires at least C# 8.0. + + The argument defined by '{0}' uses the ValidateEnumValueAttribute, but its type '{1}' is not an enumeration. + + + The ValidateEnumValueAttribute can only be used on arguments with an enum type. + + + The argument defined by '{0}' has the ArgumentConverterAttribute, and uses ValidateEnumValueAttribute properties that may not work with a custom ArgumentConverter. + + + The argument uses ValidateEnumValueAttribute properties that may not work with a custom ArgumentConverter. + \ No newline at end of file diff --git a/src/Ookii.CommandLine.Generator/SourceBuilder.cs b/src/Ookii.CommandLine.Generator/SourceBuilder.cs index be75d029..1903c4e2 100644 --- a/src/Ookii.CommandLine.Generator/SourceBuilder.cs +++ b/src/Ookii.CommandLine.Generator/SourceBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System.Reflection; using System.Text; namespace Ookii.CommandLine.Generator; @@ -9,6 +10,8 @@ internal class SourceBuilder private int _indentLevel; private bool _startOfLine = true; private bool _needArgumentSeparator; + private string? _toolName; + private string? _toolVersion; public SourceBuilder(INamespaceSymbol ns) : this(ns.IsGlobalNamespace ? null : ns.ToDisplayString()) @@ -85,6 +88,18 @@ public void CloseBlock() AppendLine("}"); } + public void AppendGeneratedCodeAttribute() + { + if (_toolName == null) + { + var assembly = Assembly.GetExecutingAssembly(); + _toolName = assembly.GetName().Name; + _toolVersion = assembly.GetCustomAttribute().InformationalVersion ?? assembly.GetName().Version.ToString(); + } + + AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{_toolName}\", \"{_toolVersion}\")]"); + } + public string GetSource() { while (_indentLevel > 0) diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 197e00a2..0859576c 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -67,10 +67,14 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? CancelMode => _compilation.GetTypeByMetadataName(NamespacePrefix + "CancelMode"); + public INamedTypeSymbol? UsageFooterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "UsageFooterAttribute"); + public INamedTypeSymbol? ArgumentValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ArgumentValidationAttribute"); public INamedTypeSymbol? ClassValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ClassValidationAttribute"); + public INamedTypeSymbol? ValidateEnumValueAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ValidateEnumValueAttribute"); + public INamedTypeSymbol? KeyValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.KeyValueSeparatorAttribute"); public INamedTypeSymbol? ArgumentConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.ArgumentConverterAttribute" diff --git a/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj index bafddff5..8ce6f074 100644 --- a/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj +++ b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj @@ -1,10 +1,10 @@  - net7.0;net6.0;net48 + net8.0;net7.0;net6.0;net48 enable enable - 11.0 + 12.0 true ookii.snk false diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index e86fa9ed..d4f8ed15 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -14,7 +14,7 @@ #nullable disable // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0038,OCL0039 +#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0038,OCL0039,OCL0040 namespace Ookii.CommandLine.Tests; @@ -451,9 +451,14 @@ public static void Arg3(int value) [CommandLineArgument] [Description("Day2 description.")] - [ValidateEnumValue] + [ValidateEnumValue(CaseSensitive = true, AllowNonDefinedValues = true, AllowCommaSeparatedValues = false)] public DayOfWeek? Day2 { get; set; } + [CommandLineArgument(IsHidden = true)] + [Description("Day3 description.")] + [ValidateEnumValue(AllowNumericValues = false)] + public DayOfWeek Day3 { get; set; } + [CommandLineArgument] [Description("NotNull description.")] [ValidateNotNull] @@ -689,3 +694,40 @@ partial class AutoPositionArguments : AutoPositionArgumentsBase [CommandLineArgument] public int Arg3 { get; set; } } + +[GeneratedParser] +[UsageFooter("Some usage footer.")] +partial class EmptyLineDescriptionArguments +{ + [CommandLineArgument] + [Description("A description with\n\na blank line.")] + public string Argument { get; set; } +} + +[GeneratedParser] +partial class DefaultValueFormatArguments +{ + [CommandLineArgument(DefaultValue = 1.5, DefaultValueFormat = "({0:0.00})")] + [Description("An argument.")] + public double Argument { get; set; } + + [CommandLineArgument(DefaultValue = 3.5)] + [Description("Another argument.")] + public double Argument2 { get; set; } +} + +[GeneratedParser] +partial class PrefixTerminationArguments +{ + [CommandLineArgument(Position = 0)] + public string Arg1 { get; set; } + + [CommandLineArgument(Position = 1)] + public string Arg2 { get; set; } + + [CommandLineArgument(Position = 2)] + public string Arg3 { get; set; } + + [CommandLineArgument(Position = 3)] + public string Arg4 { get; set; } +} diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs index 2d1cfdc8..c5fd3aa5 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs @@ -667,5 +667,111 @@ Displays this help message. -Version [] Displays version information. +".ReplaceLineEndings(); + + private static readonly string _expectedEmptyLineDefaultUsage = @"Usage: test [-Argument ] [-Help] [-Version] + + -Argument + A description with + +a blank line. + + -Help [] (-?, -h) + Displays this help message. + + -Version [] + Displays version information. + +Some usage footer. + +".ReplaceLineEndings(); + + private static readonly string _expectedEmptyLineIndentAfterBlankLineUsage = @"Usage: test [-Argument ] [-Help] [-Version] + + -Argument + A description with + + a blank line. + + -Help [] (-?, -h) + Displays this help message. + + -Version [] + Displays version information. + +Some usage footer. + +".ReplaceLineEndings(); + + private static readonly string _expectedDefaultValueFormatUsage = @"Usage: test [-Argument ] [-Argument2 ] [-Help] [-Version] + + -Argument + An argument. Default value: (1.50). + + -Argument2 + Another argument. Default value: 3.5. + + -Help [] (-?, -h) + Displays this help message. + + -Version [] + Displays version information. + +".ReplaceLineEndings(); + + private static readonly string _expectedDefaultValueFormatCultureUsage = @"Usage: test [-Argument ] [-Argument2 ] [-Help] [-Version] + + -Argument + An argument. Default value: (1,50). + + -Argument2 + Another argument. Default value: 3,5. + + -Help [] (-?, -h) + Displays this help message. + + -Version [] + Displays version information. + +".ReplaceLineEndings(); + + private static readonly string _expectedFooterUsage = @"Test arguments description. + +Usage: test [-arg1] [[-other] ] [[-notSwitch] ] [[-Arg5] ] [[-other2] ] [[-Arg8] ...] -Arg6 [-Arg10...] [-Arg11] [-Arg12 ...] [-Arg13 ...] [-Arg14 ...] [-Arg15 >] [-Arg3 ] [-Arg7] [-Arg9 ] [-Help] [-Version] + + -arg1 + Arg1 description. + + -other + Arg2 description. Default value: 42. + + -notSwitch + Default value: False. + + -Arg5 + Arg5 description. + + -other2 + Arg4 description. Default value: 47. + + -Arg6 (-Alias1, -Alias2) + Arg6 description. + + -Arg12 + Default value: 42. + + -Arg7 [] (-Alias3) + + + -Arg9 + Must be between 0 and 100. + + -Help [] (-?, -h) + Displays this help message. + + -Version [] + Displays version information. + +This is a custom footer. ".ReplaceLineEndings(); } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 0b9b2be8..3019c373 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -44,11 +44,11 @@ public void ConstructorEmptyArgumentsTest(ProviderKind kind) Assert.AreEqual("Ookii.CommandLine Unit Tests", target.ApplicationFriendlyName); Assert.AreEqual(string.Empty, target.Description); Assert.AreEqual(2, target.Arguments.Length); - VerifyArguments(target.Arguments, new[] - { - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + VerifyArguments(target.Arguments, + [ + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -67,15 +67,15 @@ public void ConstructorTest(ProviderKind kind) Assert.AreEqual("Friendly name", target.ApplicationFriendlyName); Assert.AreEqual("Test arguments description.", target.Description); Assert.AreEqual(18, target.Arguments.Length); - VerifyArguments(target.Arguments, new[] - { + VerifyArguments(target.Arguments, + [ new ExpectedArgument("arg1", typeof(string)) { MemberName = "Arg1", Position = 0, IsRequired = true, Description = "Arg1 description." }, new ExpectedArgument("other", typeof(int)) { MemberName = "Arg2", Position = 1, DefaultValue = 42, Description = "Arg2 description.", ValueDescription = "Number" }, new ExpectedArgument("notSwitch", typeof(bool)) { MemberName = "NotSwitch", Position = 2, DefaultValue = false }, new ExpectedArgument("Arg5", typeof(float)) { Position = 3, Description = "Arg5 description.", DefaultValue = 1.0f }, new ExpectedArgument("other2", typeof(int)) { MemberName = "Arg4", Position = 4, DefaultValue = 47, Description = "Arg4 description.", ValueDescription = "Number" }, new ExpectedArgument("Arg8", typeof(DayOfWeek[]), ArgumentKind.MultiValue) { ElementType = typeof(DayOfWeek), Position = 5 }, - new ExpectedArgument("Arg6", typeof(string)) { Position = null, IsRequired = true, Description = "Arg6 description.", Aliases = new[] { "Alias1", "Alias2" } }, + new ExpectedArgument("Arg6", typeof(string)) { Position = null, IsRequired = true, Description = "Arg6 description.", Aliases = ["Alias1", "Alias2"] }, new ExpectedArgument("Arg10", typeof(bool[]), ArgumentKind.MultiValue) { ElementType = typeof(bool), Position = null, IsSwitch = true }, new ExpectedArgument("Arg11", typeof(bool?)) { ElementType = typeof(bool), Position = null, ValueDescription = "Boolean", IsSwitch = true }, new ExpectedArgument("Arg12", typeof(Collection), ArgumentKind.MultiValue) { ElementType = typeof(int), Position = null, DefaultValue = 42 }, @@ -83,11 +83,11 @@ public void ConstructorTest(ProviderKind kind) new ExpectedArgument("Arg14", typeof(IDictionary), ArgumentKind.Dictionary) { ElementType = typeof(KeyValuePair), ValueDescription = "String=Int32" }, new ExpectedArgument("Arg15", typeof(KeyValuePair)) { ValueDescription = "KeyValuePair" }, new ExpectedArgument("Arg3", typeof(string)) { Position = null }, - new ExpectedArgument("Arg7", typeof(bool)) { Position = null, IsSwitch = true, Aliases = new[] { "Alias3" } }, + new ExpectedArgument("Arg7", typeof(bool)) { Position = null, IsSwitch = true, Aliases = ["Alias3"] }, new ExpectedArgument("Arg9", typeof(int?)) { ElementType = typeof(int), Position = null, ValueDescription = "Int32" }, - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -117,21 +117,21 @@ public void ParseTest(ProviderKind kind) // All positional arguments except array TestParse(target, "val1 2 true 5.5 4 -arg6 arg6", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6"); // All positional arguments including array - TestParse(target, "val1 2 true 5.5 4 -arg6 arg6 Monday Tuesday", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6", arg8: new[] { DayOfWeek.Monday, DayOfWeek.Tuesday }); + TestParse(target, "val1 2 true 5.5 4 -arg6 arg6 Monday Tuesday", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6", arg8: [DayOfWeek.Monday, DayOfWeek.Tuesday]); // All positional arguments including array, which is specified by name first and then by position - TestParse(target, "val1 2 true 5.5 4 -arg6 arg6 -arg8 Monday Tuesday", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6", arg8: new[] { DayOfWeek.Monday, DayOfWeek.Tuesday }); + TestParse(target, "val1 2 true 5.5 4 -arg6 arg6 -arg8 Monday Tuesday", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6", arg8: [DayOfWeek.Monday, DayOfWeek.Tuesday]); // Some positional arguments using names, in order TestParse(target, "-arg1 val1 2 true -arg5 5.5 4 -arg6 arg6", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6"); // Some position arguments using names, out of order (also uses : and - for one of them to mix things up) TestParse(target, "-other 2 val1 -arg5:5.5 true 4 -arg6 arg6", "val1", 2, true, arg4: 4, arg5: 5.5f, arg6: "arg6"); // All arguments - TestParse(target, "val1 2 true -arg3 val3 -other2:4 5.5 -arg6 val6 -arg7 -arg8 Monday -arg8 Tuesday -arg9 9 -arg10 -arg10 -arg10:false -arg11:false -arg12 12 -arg12 13 -arg13 foo=13 -arg13 bar=14 -arg14 hello=1 -arg14 bye=2 -arg15 something=5", "val1", 2, true, "val3", 4, 5.5f, "val6", true, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday }, 9, new[] { true, true, false }, false, new[] { 12, 13 }, new Dictionary() { { "foo", 13 }, { "bar", 14 } }, new Dictionary() { { "hello", 1 }, { "bye", 2 } }, new KeyValuePair("something", 5)); + TestParse(target, "val1 2 true -arg3 val3 -other2:4 5.5 -arg6 val6 -arg7 -arg8 Monday -arg8 Tuesday -arg9 9 -arg10 -arg10 -arg10:false -arg11:false -arg12 12 -arg12 13 -arg13 foo=13 -arg13 bar=14 -arg14 hello=1 -arg14 bye=2 -arg15 something=5", "val1", 2, true, "val3", 4, 5.5f, "val6", true, [DayOfWeek.Monday, DayOfWeek.Tuesday], 9, [true, true, false], false, [12, 13], new Dictionary() { { "foo", 13 }, { "bar", 14 } }, new Dictionary() { { "hello", 1 }, { "bye", 2 } }, new KeyValuePair("something", 5)); // Using aliases TestParse(target, "val1 2 -alias1 valalias6 -alias3", "val1", 2, arg6: "valalias6", arg7: true); // Long prefix cannot be used - CheckThrows(target, new[] { "val1", "2", "--arg6", "val6" }, CommandLineArgumentErrorCategory.UnknownArgument, "-arg6", remainingArgumentCount: 2); + CheckThrows(target, ["val1", "2", "--arg6", "val6"], CommandLineArgumentErrorCategory.UnknownArgument, "-arg6", remainingArgumentCount: 2); // Short name cannot be used - CheckThrows(target, new[] { "val1", "2", "-arg6", "val6", "-a:5.5" }, CommandLineArgumentErrorCategory.UnknownArgument, "a", remainingArgumentCount: 1); + CheckThrows(target, ["val1", "2", "-arg6", "val6", "-a:5.5"], CommandLineArgumentErrorCategory.UnknownArgument, "a", remainingArgumentCount: 1); } [TestMethod] @@ -140,7 +140,7 @@ public void ParseTestEmptyArguments(ProviderKind kind) { var target = CreateParser(kind); // This test was added because version 2.0 threw an IndexOutOfRangeException when you tried to specify a positional argument when there were no positional arguments defined. - CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 2); + CheckThrows(target, ["Foo", "Bar"], CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 2); } [TestMethod] @@ -150,7 +150,7 @@ public void ParseTestTooManyArguments(ProviderKind kind) var target = CreateParser(kind); // Only accepts one positional argument. - CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 1); + CheckThrows(target, ["Foo", "Bar"], CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 1); } [TestMethod] @@ -161,7 +161,7 @@ public void ParseTestPropertySetterThrows(ProviderKind kind) // No remaining arguments; exception happens after parsing finishes. CheckThrows(target, - new[] { "-ThrowingArgument", "-5" }, + ["-ThrowingArgument", "-5"], CommandLineArgumentErrorCategory.ApplyValueError, "ThrowingArgument", typeof(ArgumentOutOfRangeException)); @@ -186,14 +186,14 @@ public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) { var target = CreateParser(kind); - var args = target.Parse(new[] { "-DuplicateKeys", "Foo=1", "-DuplicateKeys", "Bar=2", "-DuplicateKeys", "Foo=3" }); + var args = target.Parse(["-DuplicateKeys", "Foo=1", "-DuplicateKeys", "Bar=2", "-DuplicateKeys", "Foo=3"]); Assert.IsNotNull(args); Assert.AreEqual(2, args.DuplicateKeys.Count); Assert.AreEqual(3, args.DuplicateKeys["Foo"]); Assert.AreEqual(2, args.DuplicateKeys["Bar"]); CheckThrows(target, - new[] { "-NoDuplicateKeys", "Foo=1", "-NoDuplicateKeys", "Bar=2", "-NoDuplicateKeys", "Foo=3" }, + ["-NoDuplicateKeys", "Foo=1", "-NoDuplicateKeys", "Bar=2", "-NoDuplicateKeys", "Foo=3"], CommandLineArgumentErrorCategory.InvalidDictionaryValue, "NoDuplicateKeys", typeof(ArgumentException), @@ -206,7 +206,7 @@ public void ParseTestMultiValueSeparator(ProviderKind kind) { var target = CreateParser(kind); - var args = target.Parse(new[] { "-NoSeparator", "Value1,Value2", "-NoSeparator", "Value3", "-Separator", "Value1,Value2", "-Separator", "Value3" }); + var args = target.Parse(["-NoSeparator", "Value1,Value2", "-NoSeparator", "Value3", "-Separator", "Value1,Value2", "-Separator", "Value3"]); Assert.IsNotNull(args); CollectionAssert.AreEqual(new[] { "Value1,Value2", "Value3" }, args.NoSeparator); CollectionAssert.AreEqual(new[] { "Value1", "Value2", "Value3" }, args.Separator); @@ -218,18 +218,18 @@ public void ParseTestNameValueSeparator(ProviderKind kind) { var target = CreateParser(kind); CollectionAssert.AreEquivalent(new[] { ':', '=' }, target.NameValueSeparators); - var args = CheckSuccess(target, new[] { "-Argument1:test", "-Argument2:foo:bar" }); + var args = CheckSuccess(target, ["-Argument1:test", "-Argument2:foo:bar"]); Assert.IsNotNull(args); Assert.AreEqual("test", args.Argument1); Assert.AreEqual("foo:bar", args.Argument2); - args = CheckSuccess(target, new[] { "-Argument1=test", "-Argument2=foo:bar" }); + args = CheckSuccess(target, ["-Argument1=test", "-Argument2=foo:bar"]); Assert.AreEqual("test", args.Argument1); Assert.AreEqual("foo:bar", args.Argument2); - args = CheckSuccess(target, new[] { "-Argument2:foo=bar" }); + args = CheckSuccess(target, ["-Argument2:foo=bar"]); Assert.AreEqual("foo=bar", args.Argument2); CheckThrows(target, - new[] { "-Argument1>test" }, + ["-Argument1>test"], CommandLineArgumentErrorCategory.UnknownArgument, "Argument1>test", remainingArgumentCount: 1); @@ -240,18 +240,18 @@ public void ParseTestNameValueSeparator(ProviderKind kind) }; target = CreateParser(kind, options); - args = target.Parse(new[] { "-Argument1>test", "-Argument2>foo>bar" }); + args = target.Parse(["-Argument1>test", "-Argument2>foo>bar"]); Assert.IsNotNull(args); Assert.AreEqual("test", args.Argument1); Assert.AreEqual("foo>bar", args.Argument2); CheckThrows(target, - new[] { "-Argument1:test" }, + ["-Argument1:test"], CommandLineArgumentErrorCategory.UnknownArgument, "Argument1:test", remainingArgumentCount: 1); CheckThrows(target, - new[] { "-Argument1=test" }, + ["-Argument1=test"], CommandLineArgumentErrorCategory.UnknownArgument, "Argument1=test", remainingArgumentCount: 1); @@ -267,11 +267,11 @@ public void ParseTestKeyValueSeparator(ProviderKind kind) Assert.AreEqual("<=>", target.GetArgument("CustomSeparator")!.DictionaryInfo!.KeyValueSeparator); Assert.AreEqual("String<=>String", target.GetArgument("CustomSeparator")!.ValueDescription); - var result = CheckSuccess(target, new[] { "-CustomSeparator", "foo<=>bar", "-CustomSeparator", "baz<=>contains<=>separator", "-CustomSeparator", "hello<=>" }); + var result = CheckSuccess(target, ["-CustomSeparator", "foo<=>bar", "-CustomSeparator", "baz<=>contains<=>separator", "-CustomSeparator", "hello<=>"]); Assert.IsNotNull(result); CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", "bar"), KeyValuePair.Create("baz", "contains<=>separator"), KeyValuePair.Create("hello", "") }, result.CustomSeparator); CheckThrows(target, - new[] { "-CustomSeparator", "foo=bar" }, + ["-CustomSeparator", "foo=bar"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "CustomSeparator", typeof(FormatException), @@ -280,7 +280,7 @@ public void ParseTestKeyValueSeparator(ProviderKind kind) // Inner exception is FormatException because what throws here is trying to convert // ">bar" to int. CheckThrows(target, - new[] { "-DefaultSeparator", "foo<=>bar" }, + ["-DefaultSeparator", "foo<=>bar"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "DefaultSeparator", typeof(FormatException), @@ -459,6 +459,82 @@ public void TestWriteUsageCustomIndent(ProviderKind kind) Assert.AreEqual(_expectedCustomIndentUsage, actual); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageIndentAfterBlankLine(ProviderKind kind) + { + var options = new ParseOptions() + { + UsageWriter = new UsageWriter() + { + ExecutableName = _executableName, + } + }; + + var target = CreateParser(kind, options); + string actual = target.GetUsage(); + Assert.AreEqual(_expectedEmptyLineDefaultUsage, actual); + + options.UsageWriter.IndentAfterEmptyLine = true; + actual = target.GetUsage(); + Assert.AreEqual(_expectedEmptyLineIndentAfterBlankLineUsage, actual); + + // Test again with a max length to make sure indents are properly reset where expected. + using var writer = LineWrappingTextWriter.ForStringWriter(80); + var usageWriter = new UsageWriter(writer) + { + ExecutableName = _executableName, + }; + + target.WriteUsage(usageWriter); + Assert.AreEqual(_expectedEmptyLineDefaultUsage, writer.ToString()); + + ((StringWriter)writer.BaseWriter).GetStringBuilder().Clear(); + writer.ResetIndent(); + usageWriter.IndentAfterEmptyLine = true; + target.WriteUsage(usageWriter); + Assert.AreEqual(_expectedEmptyLineIndentAfterBlankLineUsage, writer.ToString()); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageDefaultValueFormat(ProviderKind kind) + { + var options = new ParseOptions() + { + UsageWriter = new UsageWriter() + { + ExecutableName = _executableName, + } + }; + + var parser = CreateParser(kind, options); + string actual = parser.GetUsage(); + Assert.AreEqual(_expectedDefaultValueFormatUsage, actual); + + // Stream culture should be ignored for the default value in favor of the parser culture. + options.Culture = CultureInfo.GetCultureInfo("nl-NL"); + actual = parser.GetUsage(); + Assert.AreEqual(_expectedDefaultValueFormatCultureUsage, actual); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageFooter(ProviderKind kind) + { + var options = new ParseOptions() + { + UsageWriter = new CustomUsageWriter() + { + ExecutableName = _executableName + }, + }; + + var target = CreateParser(kind, options); + string actual = target.GetUsage(); + Assert.AreEqual(_expectedFooterUsage, actual); + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestStaticParse(ProviderKind kind) @@ -477,7 +553,7 @@ public void TestStaticParse(ProviderKind kind) } }; - var result = StaticParse(kind, new[] { "foo", "-Arg6", "bar" }, options); + var result = StaticParse(kind, ["foo", "-Arg6", "bar"], options); Assert.IsNotNull(result); Assert.AreEqual("foo", result.Arg1); Assert.AreEqual("bar", result.Arg6); @@ -491,7 +567,7 @@ public void TestStaticParse(ProviderKind kind) output.GetStringBuilder().Clear(); error.GetStringBuilder().Clear(); - result = StaticParse(kind, new[] { "-Help" }, options); + result = StaticParse(kind, ["-Help"], options); Assert.IsNull(result); Assert.AreEqual(0, error.ToString().Length); Assert.AreEqual(_expectedDefaultUsage, output.ToString()); @@ -515,7 +591,7 @@ public void TestStaticParse(ProviderKind kind) // Still get full help with -Help arg. output.GetStringBuilder().Clear(); error.GetStringBuilder().Clear(); - result = StaticParse(kind, new[] { "-Help" }, options); + result = StaticParse(kind, ["-Help"], options); Assert.IsNull(result); Assert.AreEqual(0, error.ToString().Length); Assert.AreEqual(_expectedDefaultUsage, output.ToString()); @@ -528,7 +604,7 @@ public void TestCancelParsing(ProviderKind kind) var parser = CreateParser(kind); // Don't cancel if -DoesCancel not specified. - var result = parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); + var result = parser.Parse(["-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar"]); Assert.IsNotNull(result); Assert.IsFalse(parser.HelpRequested); Assert.IsTrue(result.DoesNotCancel); @@ -540,12 +616,12 @@ public void TestCancelParsing(ProviderKind kind) Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); // Cancel if -DoesCancel specified. - result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); + result = parser.Parse(["-Argument1", "foo", "-DoesCancel", "-Argument2", "bar"]); Assert.IsNull(result); Assert.IsTrue(parser.HelpRequested); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); - AssertSpanEqual(new[] { "-Argument2", "bar" }.AsSpan(), parser.ParseResult.RemainingArguments.Span); + AssertSpanEqual(["-Argument2", "bar"], parser.ParseResult.RemainingArguments.Span); Assert.AreEqual("DoesCancel", parser.ParseResult.ArgumentName); Assert.IsTrue(parser.GetArgument("Argument1")!.HasValue); Assert.AreEqual("foo", (string?)parser.GetArgument("Argument1")!.Value); @@ -566,7 +642,7 @@ static void handler1(object? sender, ArgumentParsedEventArgs e) } parser.ArgumentParsed += handler1; - result = parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); + result = parser.Parse(["-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar"]); Assert.IsNull(result); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); @@ -594,7 +670,7 @@ static void handler2(object? sender, ArgumentParsedEventArgs e) } parser.ArgumentParsed += handler2; - result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); + result = parser.Parse(["-Argument1", "foo", "-DoesCancel", "-Argument2", "bar"]); Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.ArgumentName); Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); @@ -606,7 +682,7 @@ static void handler2(object? sender, ArgumentParsedEventArgs e) Assert.AreEqual("bar", result.Argument2); // Automatic help argument should cancel. - result = parser.Parse(new[] { "-Help" }); + result = parser.Parse(["-Help"]); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); Assert.AreEqual("Help", parser.ParseResult.ArgumentName); @@ -620,7 +696,7 @@ static void handler2(object? sender, ArgumentParsedEventArgs e) public void TestCancelParsingSuccess(ProviderKind kind) { var parser = CreateParser(kind); - var result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancelWithSuccess", "-Argument2", "bar" }); + var result = parser.Parse(["-Argument1", "foo", "-DoesCancelWithSuccess", "-Argument2", "bar"]); Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); Assert.AreEqual("DoesCancelWithSuccess", parser.ParseResult.ArgumentName); AssertSpanEqual(new[] { "-Argument2", "bar" }.AsSpan(), parser.ParseResult.RemainingArguments.Span); @@ -633,7 +709,7 @@ public void TestCancelParsingSuccess(ProviderKind kind) Assert.IsNull(result.Argument2); // No remaining arguments. - result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancelWithSuccess" }); + result = parser.Parse(["-Argument1", "foo", "-DoesCancelWithSuccess"]); Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); Assert.AreEqual("DoesCancelWithSuccess", parser.ParseResult.ArgumentName); Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); @@ -693,19 +769,19 @@ public void TestParseOptionsAttribute(ProviderKind kind) [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestCulture(ProviderKind kind) { - var result = StaticParse(kind, new[] { "-Argument", "5.5" }); + var result = StaticParse(kind, ["-Argument", "5.5"]); Assert.IsNotNull(result); Assert.AreEqual(5.5, result.Argument); - result = StaticParse(kind, new[] { "-Argument", "5,5" }); + result = StaticParse(kind, ["-Argument", "5,5"]); Assert.IsNotNull(result); // , was interpreted as a thousands separator. Assert.AreEqual(55, result.Argument); var options = new ParseOptions { Culture = new CultureInfo("nl-NL") }; - result = StaticParse(kind, new[] { "-Argument", "5,5" }, options); + result = StaticParse(kind, ["-Argument", "5,5"], options); Assert.IsNotNull(result); Assert.AreEqual(5.5, result.Argument); - result = StaticParse(kind, new[] { "-Argument", "5,5" }); + result = StaticParse(kind, ["-Argument", "5,5"]); Assert.IsNotNull(result); // . was interpreted as a thousands separator. Assert.AreEqual(55, result.Argument); @@ -730,7 +806,7 @@ public void TestLongShortMode(ProviderKind kind) Assert.AreEqual('\0', parser.GetArgument("bar")!.ShortName); Assert.IsFalse(parser.GetArgument("bar")!.HasShortName); - var result = CheckSuccess(parser, new[] { "-f", "5", "--bar", "6", "-a", "7", "--arg1", "8", "-s" }); + var result = CheckSuccess(parser, ["-f", "5", "--bar", "6", "-a", "7", "--arg1", "8", "-s"]); Assert.AreEqual(5, result.Foo); Assert.AreEqual(6, result.Bar); Assert.AreEqual(7, result.Arg2); @@ -740,26 +816,26 @@ public void TestLongShortMode(ProviderKind kind) Assert.IsFalse(result.Switch3); // Combine switches. - result = CheckSuccess(parser, new[] { "-su" }); + result = CheckSuccess(parser, ["-su"]); Assert.IsTrue(result.Switch1); Assert.IsFalse(LongShortArguments.Switch2Value); Assert.IsTrue(result.Switch3); // Use a short alias. - result = CheckSuccess(parser, new[] { "-b", "5" }); + result = CheckSuccess(parser, ["-b", "5"]); Assert.AreEqual(5, result.Arg2); // Combining non-switches is an error. - CheckThrows(parser, new[] { "-sf" }, CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, "sf", remainingArgumentCount: 1); + CheckThrows(parser, ["-sf"], CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, "sf", remainingArgumentCount: 1); // Can't use long argument prefix with short names. - CheckThrows(parser, new[] { "--s" }, CommandLineArgumentErrorCategory.UnknownArgument, "s", remainingArgumentCount: 1); + CheckThrows(parser, ["--s"], CommandLineArgumentErrorCategory.UnknownArgument, "s", remainingArgumentCount: 1); // And vice versa. - CheckThrows(parser, new[] { "-Switch1" }, CommandLineArgumentErrorCategory.UnknownArgument, "w", remainingArgumentCount: 1); + CheckThrows(parser, ["-Switch1"], CommandLineArgumentErrorCategory.UnknownArgument, "w", remainingArgumentCount: 1); // Short alias is ignored on an argument without a short name. - CheckThrows(parser, new[] { "-c" }, CommandLineArgumentErrorCategory.UnknownArgument, "c", remainingArgumentCount: 1); + CheckThrows(parser, ["-c"], CommandLineArgumentErrorCategory.UnknownArgument, "c", remainingArgumentCount: 1); } [TestMethod] @@ -773,48 +849,101 @@ public void TestMethodArguments(ProviderKind kind) Assert.IsNull(parser.GetArgument("NotStatic")); Assert.IsNull(parser.GetArgument("NotPublic")); - CheckSuccess(parser, new[] { "-NoCancel" }); + CheckSuccess(parser, ["-NoCancel"]); Assert.AreEqual(nameof(MethodArguments.NoCancel), MethodArguments.CalledMethodName); - CheckCanceled(parser, new[] { "-Cancel", "Foo" }, "Cancel", false, 1); + CheckCanceled(parser, ["-Cancel", "Foo"], "Cancel", false, 1); Assert.AreEqual(nameof(MethodArguments.Cancel), MethodArguments.CalledMethodName); - CheckCanceled(parser, new[] { "-CancelWithHelp" }, "CancelWithHelp", true, 0); + CheckCanceled(parser, ["-CancelWithHelp"], "CancelWithHelp", true, 0); Assert.AreEqual(nameof(MethodArguments.CancelWithHelp), MethodArguments.CalledMethodName); - CheckSuccess(parser, new[] { "-CancelWithValue", "1" }); + CheckSuccess(parser, ["-CancelWithValue", "1"]); Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); Assert.AreEqual(1, MethodArguments.Value); - CheckCanceled(parser, new[] { "-CancelWithValue", "-1" }, "CancelWithValue", false); + CheckCanceled(parser, ["-CancelWithValue", "-1"], "CancelWithValue", false); Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); Assert.AreEqual(-1, MethodArguments.Value); - CheckSuccess(parser, new[] { "-CancelWithValueAndHelp", "1" }); + CheckSuccess(parser, ["-CancelWithValueAndHelp", "1"]); Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); Assert.AreEqual(1, MethodArguments.Value); - CheckCanceled(parser, new[] { "-CancelWithValueAndHelp", "-1", "bar" }, "CancelWithValueAndHelp", true, 1); + CheckCanceled(parser, ["-CancelWithValueAndHelp", "-1", "bar"], "CancelWithValueAndHelp", true, 1); Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); Assert.AreEqual(-1, MethodArguments.Value); - CheckSuccess(parser, new[] { "-NoReturn" }); + CheckSuccess(parser, ["-NoReturn"]); Assert.AreEqual(nameof(MethodArguments.NoReturn), MethodArguments.CalledMethodName); - CheckSuccess(parser, new[] { "42" }); + CheckSuccess(parser, ["42"]); Assert.AreEqual(nameof(MethodArguments.Positional), MethodArguments.CalledMethodName); Assert.AreEqual(42, MethodArguments.Value); - CheckCanceled(parser, new[] { "-CancelModeAbort", "Foo" }, "CancelModeAbort", false, 1); + CheckCanceled(parser, ["-CancelModeAbort", "Foo"], "CancelModeAbort", false, 1); Assert.AreEqual(nameof(MethodArguments.CancelModeAbort), MethodArguments.CalledMethodName); - CheckSuccess(parser, new[] { "-CancelModeSuccess", "Foo" }, "CancelModeSuccess", 1); + CheckSuccess(parser, ["-CancelModeSuccess", "Foo"], "CancelModeSuccess", 1); Assert.AreEqual(nameof(MethodArguments.CancelModeSuccess), MethodArguments.CalledMethodName); - CheckSuccess(parser, new[] { "-CancelModeNone" }); + CheckSuccess(parser, ["-CancelModeNone"]); Assert.AreEqual(nameof(MethodArguments.CancelModeNone), MethodArguments.CalledMethodName); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestPrefixTermination(ProviderKind kind) + { + var options = new ParseOptions() + { + PrefixTermination = PrefixTerminationMode.PositionalOnly + }; + + var parser = CreateParser(kind, options); + Assert.AreEqual("--", parser.LongArgumentNamePrefix); + Assert.AreEqual(ParsingMode.Default, parser.Mode); + var result = CheckSuccess(parser, ["Foo", "--", "-Arg4", "Bar"]); + Assert.AreEqual("Foo", result.Arg1); + Assert.AreEqual("-Arg4", result.Arg2); + Assert.AreEqual("Bar", result.Arg3); + Assert.IsNull(result.Arg4); + options.PrefixTermination = PrefixTerminationMode.CancelWithSuccess; + result = CheckSuccess(parser, ["Foo", "--", "-Arg4", "Bar"], "--", 2); + Assert.AreEqual("Foo", result.Arg1); + Assert.IsNull(result.Arg2); + Assert.IsNull(result.Arg3); + Assert.IsNull(result.Arg4); + options.PrefixTermination = PrefixTerminationMode.CancelWithSuccess; + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestPrefixTerminationLongShort(ProviderKind kind) + { + var options = new ParseOptions() + { + IsPosix = true, + PrefixTermination = PrefixTerminationMode.PositionalOnly + }; + + var parser = CreateParser(kind, options); + Assert.AreEqual("--", parser.LongArgumentNamePrefix); + Assert.AreEqual(ParsingMode.LongShort, parser.Mode); + var result = CheckSuccess(parser, ["--arg4", "Foo", "--", "--arg1", "Bar"]); + Assert.AreEqual("Foo", result.Arg4); + Assert.AreEqual("--arg1", result.Arg1); + Assert.AreEqual("Bar", result.Arg2); + Assert.IsNull(result.Arg3); + options.PrefixTermination = PrefixTerminationMode.CancelWithSuccess; + result = CheckSuccess(parser, ["Foo", "--", "--arg4", "Bar"], "--", 2); + Assert.AreEqual("Foo", result.Arg1); + Assert.IsNull(result.Arg2); + Assert.IsNull(result.Arg3); + Assert.IsNull(result.Arg4); + options.PrefixTermination = PrefixTerminationMode.CancelWithSuccess; + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestAutomaticArgumentConflict(ProviderKind kind) @@ -857,15 +986,15 @@ public void TestNameTransformPascalCase(ProviderKind kind) }; var parser = CreateParser(kind, options); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("TestArg", typeof(string)) { MemberName = "testArg", Position = 0, IsRequired = true }, new ExpectedArgument("ExplicitName", typeof(int)) { MemberName = "Explicit" }, - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("TestArg2", typeof(int)) { MemberName = "TestArg2" }, new ExpectedArgument("TestArg3", typeof(int)) { MemberName = "__test__arg3__" }, new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -878,15 +1007,15 @@ public void TestNameTransformCamelCase(ProviderKind kind) }; var parser = CreateParser(kind, options); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("testArg", typeof(string)) { MemberName = "testArg", Position = 0, IsRequired = true }, new ExpectedArgument("ExplicitName", typeof(int)) { MemberName = "Explicit" }, - new ExpectedArgument("help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("testArg2", typeof(int)) { MemberName = "TestArg2" }, new ExpectedArgument("testArg3", typeof(int)) { MemberName = "__test__arg3__" }, new ExpectedArgument("version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -899,15 +1028,15 @@ public void TestNameTransformSnakeCase(ProviderKind kind) }; var parser = CreateParser(kind, options); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("test_arg", typeof(string)) { MemberName = "testArg", Position = 0, IsRequired = true }, new ExpectedArgument("ExplicitName", typeof(int)) { MemberName = "Explicit" }, - new ExpectedArgument("help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("test_arg2", typeof(int)) { MemberName = "TestArg2" }, new ExpectedArgument("test_arg3", typeof(int)) { MemberName = "__test__arg3__" }, new ExpectedArgument("version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -920,15 +1049,15 @@ public void TestNameTransformDashCase(ProviderKind kind) }; var parser = CreateParser(kind, options); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("test-arg", typeof(string)) { MemberName = "testArg", Position = 0, IsRequired = true }, new ExpectedArgument("ExplicitName", typeof(int)) { MemberName = "Explicit" }, - new ExpectedArgument("help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("test-arg2", typeof(int)) { MemberName = "TestArg2" }, new ExpectedArgument("test-arg3", typeof(int)) { MemberName = "__test__arg3__" }, new ExpectedArgument("version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -941,13 +1070,13 @@ public void TestValueDescriptionTransform(ProviderKind kind) }; var parser = CreateParser(kind, options); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("Arg1", typeof(FileInfo)) { ValueDescription = "file-info" }, new ExpectedArgument("Arg2", typeof(int)) { ValueDescription = "int32" }, - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { ValueDescription = "boolean", MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { ValueDescription = "boolean", MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { ValueDescription = "boolean", MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -959,59 +1088,73 @@ public void TestValidation(ProviderKind kind) var parser = CreateParser(kind); // Range validator on property - CheckThrows(parser, new[] { "-Arg1", "0" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); - var result = CheckSuccess(parser, new[] { "-Arg1", "1" }); + CheckThrows(parser, ["-Arg1", "0"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); + var result = CheckSuccess(parser, ["-Arg1", "1"]); Assert.AreEqual(1, result.Arg1); - result = CheckSuccess(parser, new[] { "-Arg1", "5" }); + result = CheckSuccess(parser, ["-Arg1", "5"]); Assert.AreEqual(5, result.Arg1); - CheckThrows(parser, new[] { "-Arg1", "6" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); + CheckThrows(parser, ["-Arg1", "6"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); // Not null or empty on ctor parameter - CheckThrows(parser, new[] { "" }, CommandLineArgumentErrorCategory.ValidationFailed, "arg2", remainingArgumentCount: 1); - result = CheckSuccess(parser, new[] { " " }); + CheckThrows(parser, [""], CommandLineArgumentErrorCategory.ValidationFailed, "arg2", remainingArgumentCount: 1); + result = CheckSuccess(parser, [" "]); Assert.AreEqual(" ", result.Arg2); // Multiple validators on method - CheckThrows(parser, new[] { "-Arg3", "1238" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); + CheckThrows(parser, ["-Arg3", "1238"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); Assert.AreEqual(0, ValidationArguments.Arg3Value); - CheckThrows(parser, new[] { "-Arg3", "123" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); + CheckThrows(parser, ["-Arg3", "123"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); Assert.AreEqual(0, ValidationArguments.Arg3Value); - CheckThrows(parser, new[] { "-Arg3", "7001" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); + CheckThrows(parser, ["-Arg3", "7001"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); // Range validation is done after setting the value, so this was set! Assert.AreEqual(7001, ValidationArguments.Arg3Value); - CheckSuccess(parser, new[] { "-Arg3", "1023" }); + CheckSuccess(parser, ["-Arg3", "1023"]); Assert.AreEqual(1023, ValidationArguments.Arg3Value); // Validator on multi-value argument - CheckThrows(parser, new[] { "-Arg4", "foo;bar;bazz" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4", remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Arg4", "foo", "-Arg4", "bar", "-Arg4", "bazz" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4", remainingArgumentCount: 2); - result = CheckSuccess(parser, new[] { "-Arg4", "foo;bar" }); + CheckThrows(parser, ["-Arg4", "foo;bar;bazz"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg4", remainingArgumentCount: 2); + CheckThrows(parser, ["-Arg4", "foo", "-Arg4", "bar", "-Arg4", "bazz"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg4", remainingArgumentCount: 2); + result = CheckSuccess(parser, ["-Arg4", "foo;bar"]); CollectionAssert.AreEqual(new[] { "foo", "bar" }, result.Arg4); - result = CheckSuccess(parser, new[] { "-Arg4", "foo", "-Arg4", "bar" }); + result = CheckSuccess(parser, ["-Arg4", "foo", "-Arg4", "bar"]); CollectionAssert.AreEqual(new[] { "foo", "bar" }, result.Arg4); // Count validator // No remaining arguments because validation happens after parsing. - CheckThrows(parser, new[] { "-Arg4", "foo" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); - CheckThrows(parser, new[] { "-Arg4", "foo;bar;baz;ban;bap" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); - result = CheckSuccess(parser, new[] { "-Arg4", "foo;bar;baz;ban" }); + CheckThrows(parser, ["-Arg4", "foo"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); + CheckThrows(parser, ["-Arg4", "foo;bar;baz;ban;bap"], CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); + result = CheckSuccess(parser, ["-Arg4", "foo;bar;baz;ban"]); CollectionAssert.AreEqual(new[] { "foo", "bar", "baz", "ban" }, result.Arg4); // Enum validator - CheckThrows(parser, new[] { "-Day", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(ArgumentException), remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Day", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day", remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Day", "" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(ArgumentException), remainingArgumentCount: 2); - result = CheckSuccess(parser, new[] { "-Day", "1" }); + CheckThrows(parser, ["-Day", "foo"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(ArgumentException), remainingArgumentCount: 2); + CheckThrows(parser, ["-Day", "9"], CommandLineArgumentErrorCategory.ValidationFailed, "Day", remainingArgumentCount: 2); + CheckThrows(parser, ["-Day", ""], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(ArgumentException), remainingArgumentCount: 2); + result = CheckSuccess(parser, ["-Day", "1"]); Assert.AreEqual(DayOfWeek.Monday, result.Day); - CheckThrows(parser, new[] { "-Day2", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(ArgumentException), remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Day2", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day2", remainingArgumentCount: 2); - result = CheckSuccess(parser, new[] { "-Day2", "1" }); + CheckThrows(parser, ["-Day2", "foo"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(ArgumentException), remainingArgumentCount: 2); + CheckSuccess(parser, ["-Day2", "9"]); // This one allows it. + result = CheckSuccess(parser, ["-Day2", "1"]); Assert.AreEqual(DayOfWeek.Monday, result.Day2); - result = CheckSuccess(parser, new[] { "-Day2", "" }); + result = CheckSuccess(parser, ["-Day2", ""]); Assert.IsNull(result.Day2); + // Case sensitive enums + CheckSuccess(parser, ["-Day", "tuesday"]); + CheckSuccess(parser, ["-Day2", "Tuesday"]); + CheckThrows(parser, ["-Day2", "tuesday"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(ArgumentException), remainingArgumentCount: 2); + + // Disallow commas. + result = CheckSuccess(parser, ["-Day3", "Monday,Tuesday"]); + Assert.AreEqual(DayOfWeek.Wednesday, result.Day3); + CheckThrows(parser, ["-Day2", "Monday,Tuesday"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", remainingArgumentCount: 2); + + // Disallow numbers. + CheckThrows(parser, ["-Day3", "5"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day3", remainingArgumentCount: 2); + CheckThrows(parser, ["-Day3", "Tuesday,5"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day3", remainingArgumentCount: 2); + // NotNull validator with Nullable. - CheckThrows(parser, new[] { "-NotNull", "" }, CommandLineArgumentErrorCategory.ValidationFailed, "NotNull", remainingArgumentCount: 2); + CheckThrows(parser, ["-NotNull", ""], CommandLineArgumentErrorCategory.ValidationFailed, "NotNull", remainingArgumentCount: 2); } [TestMethod] @@ -1021,16 +1164,16 @@ public void TestRequires(ProviderKind kind) var parser = CreateParser(kind); // None of these have remaining arguments because validation happens after parsing. - var result = CheckSuccess(parser, new[] { "-Address", "127.0.0.1" }); + var result = CheckSuccess(parser, ["-Address", "127.0.0.1"]); Assert.AreEqual(IPAddress.Loopback, result.Address); - CheckThrows(parser, new[] { "-Port", "9000" }, CommandLineArgumentErrorCategory.DependencyFailed, "Port"); - result = CheckSuccess(parser, new[] { "-Address", "127.0.0.1", "-Port", "9000" }); + CheckThrows(parser, ["-Port", "9000"], CommandLineArgumentErrorCategory.DependencyFailed, "Port"); + result = CheckSuccess(parser, ["-Address", "127.0.0.1", "-Port", "9000"]); Assert.AreEqual(IPAddress.Loopback, result.Address); Assert.AreEqual(9000, result.Port); - CheckThrows(parser, new[] { "-Protocol", "1" }, CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); - CheckThrows(parser, new[] { "-Address", "127.0.0.1", "-Protocol", "1" }, CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); - CheckThrows(parser, new[] { "-Throughput", "10", "-Protocol", "1" }, CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); - result = CheckSuccess(parser, new[] { "-Protocol", "1", "-Address", "127.0.0.1", "-Throughput", "10" }); + CheckThrows(parser, ["-Protocol", "1"], CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); + CheckThrows(parser, ["-Address", "127.0.0.1", "-Protocol", "1"], CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); + CheckThrows(parser, ["-Throughput", "10", "-Protocol", "1"], CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); + result = CheckSuccess(parser, ["-Protocol", "1", "-Address", "127.0.0.1", "-Throughput", "10"]); Assert.AreEqual(IPAddress.Loopback, result.Address); Assert.AreEqual(10, result.Throughput); Assert.AreEqual(1, result.Protocol); @@ -1042,10 +1185,10 @@ public void TestProhibits(ProviderKind kind) { var parser = CreateParser(kind); - var result = CheckSuccess(parser, new[] { "-Path", "test" }); + var result = CheckSuccess(parser, ["-Path", "test"]); Assert.AreEqual("test", result.Path.Name); // No remaining arguments because validation happens after parsing. - CheckThrows(parser, new[] { "-Path", "test", "-Address", "127.0.0.1" }, CommandLineArgumentErrorCategory.DependencyFailed, "Path"); + CheckThrows(parser, ["-Path", "test", "-Address", "127.0.0.1"], CommandLineArgumentErrorCategory.DependencyFailed, "Path"); } [TestMethod] @@ -1105,19 +1248,19 @@ public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) Assert.IsFalse(parser.GetArgument("MultiSwitch")!.MultiValueInfo!.AllowWhiteSpaceSeparator); Assert.IsNull(parser.GetArgument("Other")!.MultiValueInfo); - var result = CheckSuccess(parser, new[] { "1", "-Multi", "2", "3", "4", "-Other", "5", "6" }); + var result = CheckSuccess(parser, ["1", "-Multi", "2", "3", "4", "-Other", "5", "6"]); Assert.AreEqual(result.Arg1, 1); Assert.AreEqual(result.Arg2, 6); Assert.AreEqual(result.Other, 5); CollectionAssert.AreEqual(new[] { 2, 3, 4 }, result.Multi); - result = CheckSuccess(parser, new[] { "-Multi", "1", "-Multi", "2" }); + result = CheckSuccess(parser, ["-Multi", "1", "-Multi", "2"]); CollectionAssert.AreEqual(new[] { 1, 2 }, result.Multi); - CheckThrows(parser, new[] { "1", "-Multi", "-Other", "5", "6" }, CommandLineArgumentErrorCategory.MissingNamedArgumentValue, "Multi", remainingArgumentCount: 4); - CheckThrows(parser, new[] { "-MultiSwitch", "true", "false" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Arg1", typeof(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, ["1", "-Multi", "-Other", "5", "6"], CommandLineArgumentErrorCategory.MissingNamedArgumentValue, "Multi", remainingArgumentCount: 4); + CheckThrows(parser, ["-MultiSwitch", "true", "false"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Arg1", typeof(FormatException), remainingArgumentCount: 2); parser.Options.AllowWhiteSpaceValueSeparator = false; - CheckThrows(parser, new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 5); + CheckThrows(parser, ["1", "-Multi:2", "2", "3", "4", "-Other", "5", "6"], CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 5); } [TestMethod] @@ -1125,7 +1268,7 @@ public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) public void TestInjection(ProviderKind kind) { var parser = CreateParser(kind); - var result = CheckSuccess(parser, new[] { "-Arg", "1" }); + var result = CheckSuccess(parser, ["-Arg", "1"]); Assert.AreSame(parser, result.Parser); Assert.AreEqual(1, result.Arg); } @@ -1135,9 +1278,9 @@ public void TestInjection(ProviderKind kind) public void TestDuplicateArguments(ProviderKind kind) { var parser = CreateParser(kind); - CheckThrows(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); + CheckThrows(parser, ["-Argument1", "foo", "-Argument1", "bar"], CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); parser.Options.DuplicateArguments = ErrorMode.Allow; - var result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); + var result = CheckSuccess(parser, ["-Argument1", "foo", "-Argument1", "bar"]); Assert.AreEqual("bar", result.Argument1); bool handlerCalled = false; @@ -1158,12 +1301,12 @@ void handler(object? sender, DuplicateArgumentEventArgs e) // Handler is not called when duplicates not allowed. parser.Options.DuplicateArguments = ErrorMode.Error; - CheckThrows(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); + CheckThrows(parser, ["-Argument1", "foo", "-Argument1", "bar"], CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); Assert.IsFalse(handlerCalled); // Now it is called. parser.Options.DuplicateArguments = ErrorMode.Allow; - result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); + result = CheckSuccess(parser, ["-Argument1", "foo", "-Argument1", "bar"]); Assert.AreEqual("bar", result.Argument1); Assert.IsTrue(handlerCalled); @@ -1171,7 +1314,7 @@ void handler(object? sender, DuplicateArgumentEventArgs e) parser.Options.DuplicateArguments = ErrorMode.Warning; handlerCalled = false; keepOldValue = true; - result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); + result = CheckSuccess(parser, ["-Argument1", "foo", "-Argument1", "bar"]); Assert.AreEqual("foo", result.Argument1); Assert.IsTrue(handlerCalled); } @@ -1194,7 +1337,7 @@ public void TestConversion(ProviderKind kind) Assert.AreEqual(10, result.NullableMulti[1]!.Value); Assert.AreEqual(11, result.Nullable); - result = CheckSuccess(parser, new[] { "-ParseNullable", "", "-NullableMulti", "1", "", "2", "-ParseNullableMulti", "3", "", "4" }); + result = CheckSuccess(parser, ["-ParseNullable", "", "-NullableMulti", "1", "", "2", "-ParseNullableMulti", "3", "", "4"]); Assert.IsNull(result.ParseNullable); Assert.AreEqual(1, result.NullableMulti[0]!.Value); Assert.IsNull(result.NullableMulti[1]); @@ -1212,8 +1355,8 @@ public void TestConversion(ProviderKind kind) public void TestConversionInvalid(ProviderKind kind) { var parser = CreateParser(kind); - CheckThrows(parser, new[] { "-Nullable", "abc" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Nullable", typeof(FormatException), 2); - CheckThrows(parser, new[] { "-Nullable", "12345678901234567890" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Nullable", typeof(OverflowException), 2); + CheckThrows(parser, ["-Nullable", "abc"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Nullable", typeof(FormatException), 2); + CheckThrows(parser, ["-Nullable", "12345678901234567890"], CommandLineArgumentErrorCategory.ArgumentValueConversion, "Nullable", typeof(OverflowException), 2); } [TestMethod] @@ -1223,13 +1366,13 @@ public void TestDerivedClass(ProviderKind kind) var parser = CreateParser(kind); Assert.AreEqual("Base class attribute.", parser.Description); Assert.AreEqual(4, parser.Arguments.Length); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("BaseArg", typeof(string), ArgumentKind.SingleValue), new ExpectedArgument("DerivedArg", typeof(int), ArgumentKind.SingleValue), - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); } [TestMethod] @@ -1259,27 +1402,27 @@ public void TestAutoPrefixAliases(ProviderKind kind) var parser = CreateParser(kind); // Shortest possible prefixes - var result = parser.Parse(new[] { "-pro", "foo", "-Po", "5", "-e" }); + var result = parser.Parse(["-pro", "foo", "-Po", "5", "-e"]); Assert.IsNotNull(result); Assert.AreEqual("foo", result.Protocol); Assert.AreEqual(5, result.Port); Assert.IsTrue(result.EnablePrefix); // Ambiguous prefix - CheckThrows(parser, new[] { "-p", "foo" }, CommandLineArgumentErrorCategory.UnknownArgument, "p", remainingArgumentCount: 2); + CheckThrows(parser, ["-p", "foo"], CommandLineArgumentErrorCategory.UnknownArgument, "p", remainingArgumentCount: 2); // Ambiguous due to alias. - CheckThrows(parser, new[] { "-pr", "foo" }, CommandLineArgumentErrorCategory.UnknownArgument, "pr", remainingArgumentCount: 2); + CheckThrows(parser, ["-pr", "foo"], CommandLineArgumentErrorCategory.UnknownArgument, "pr", remainingArgumentCount: 2); // Prefix of an alias. - result = parser.Parse(new[] { "-pre" }); + result = parser.Parse(["-pre"]); Assert.IsNotNull(result); Assert.IsTrue(result.EnablePrefix); // Disable auto prefix aliases. var options = new ParseOptions() { AutoPrefixAliases = false }; parser = CreateParser(kind, options); - CheckThrows(parser, new[] { "-pro", "foo", "-Po", "5", "-e" }, CommandLineArgumentErrorCategory.UnknownArgument, "pro", remainingArgumentCount: 5); + CheckThrows(parser, ["-pro", "foo", "-Po", "5", "-e"], CommandLineArgumentErrorCategory.UnknownArgument, "pro", remainingArgumentCount: 5); } [TestMethod] @@ -1301,17 +1444,17 @@ public void TestApplicationFriendlyName(ProviderKind kind) public void TestAutoPosition() { var parser = AutoPositionArguments.CreateParser(); - VerifyArguments(parser.Arguments, new[] - { + VerifyArguments(parser.Arguments, + [ new ExpectedArgument("BaseArg1", typeof(string), ArgumentKind.SingleValue) { Position = 0, IsRequired = true }, new ExpectedArgument("BaseArg2", typeof(int), ArgumentKind.SingleValue) { Position = 1 }, new ExpectedArgument("Arg1", typeof(string), ArgumentKind.SingleValue) { Position = 2 }, new ExpectedArgument("Arg2", typeof(int), ArgumentKind.SingleValue) { Position = 3 }, new ExpectedArgument("Arg3", typeof(int), ArgumentKind.SingleValue), new ExpectedArgument("BaseArg3", typeof(int), ArgumentKind.SingleValue), - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = ["?", "h"] }, new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); + ]); try { @@ -1323,6 +1466,101 @@ public void TestAutoPosition() } } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestUnknownArgument(ProviderKind kind) + { + var parser = CreateParser(kind); + ReadOnlyMemory expectedName = default; + ReadOnlyMemory expectedValue = default; + var expectedToken = ""; + var expectedCombined = false; + var ignore = false; + var cancel = CancelMode.None; + var eventRaised = false; + parser.UnknownArgument += (_, e) => + { + AssertMemoryEqual(expectedName, e.Name); + AssertMemoryEqual(expectedValue, e.Value); + Assert.AreEqual(expectedToken, e.Token); + Assert.AreEqual(expectedCombined, e.IsCombinedSwitchToken); + e.CancelParsing = cancel; + e.Ignore = ignore; + eventRaised = true; + }; + + expectedName = "Unknown".AsMemory(); + expectedToken = "--Unknown"; + CheckThrows(parser, ["--arg1", "5", "--Unknown", "foo"], CommandLineArgumentErrorCategory.UnknownArgument, "Unknown", remainingArgumentCount: 2); + Assert.IsTrue(eventRaised); + + eventRaised = false; + ignore = true; + var result = CheckSuccess(parser, ["--arg1", "5", "--Unknown", "1"]); + Assert.AreEqual(1, result.Foo); + Assert.AreEqual(5, result.Arg1); + Assert.IsTrue(eventRaised); + + eventRaised = false; + cancel = CancelMode.Success; + result = CheckSuccess(parser, ["--arg1", "5", "--Unknown", "1"], "Unknown", 1); + Assert.AreEqual(0, result.Foo); + Assert.AreEqual(5, result.Arg1); + Assert.IsTrue(eventRaised); + + eventRaised = false; + cancel = CancelMode.Abort; + CheckCanceled(parser, ["--arg1", "5", "--Unknown", "1"], "Unknown", false, 1); + Assert.IsTrue(eventRaised); + + // With a value. + expectedValue = "foo".AsMemory(); + expectedToken = "--Unknown:foo"; + CheckCanceled(parser, ["--arg1", "5", "--Unknown:foo", "1"], "Unknown", false, 1); + Assert.IsTrue(eventRaised); + + // Now with a short name. + eventRaised = false; + expectedName = "z".AsMemory(); + expectedValue = default; + expectedToken = "-z"; + cancel = CancelMode.None; + result = CheckSuccess(parser, ["--arg1", "5", "-z", "1"]); + Assert.AreEqual(1, result.Foo); + Assert.AreEqual(5, result.Arg1); + Assert.IsTrue(eventRaised); + + // One in a combined short name. + eventRaised = false; + expectedToken = "-szu"; + expectedCombined = true; + cancel = CancelMode.None; + result = CheckSuccess(parser, ["--arg1", "5", "-szu", "1"]); + Assert.AreEqual(1, result.Foo); + Assert.AreEqual(5, result.Arg1); + Assert.IsTrue(result.Switch1); + Assert.IsTrue(result.Switch3); + Assert.IsTrue(eventRaised); + + // Positional + eventRaised = false; + expectedName = default; + expectedValue = "4".AsMemory(); + expectedToken = "4"; + expectedCombined = false; + result = CheckSuccess(parser, ["1", "2", "3", "4", "--arg1", "5"]); + Assert.AreEqual(1, result.Foo); + Assert.AreEqual(2, result.Bar); + Assert.AreEqual(3, result.Arg2); + Assert.AreEqual(5, result.Arg1); + Assert.IsTrue(eventRaised); + + eventRaised = false; + ignore = false; + CheckThrows(parser, ["1", "2", "3", "4", "--arg1", "5"], CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 3); + Assert.IsTrue(eventRaised); + } + private class ExpectedArgument { public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) @@ -1330,6 +1568,7 @@ public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind Name = name; Type = type; Kind = kind; + } public string Name { get; set; } @@ -1373,6 +1612,15 @@ private static void VerifyArgument(CommandLineArgument? argument, ExpectedArgume Assert.IsFalse(argument.HasValue); CollectionAssert.AreEqual(expected.Aliases ?? Array.Empty(), argument.Aliases); CollectionAssert.AreEqual(expected.ShortAliases ?? Array.Empty(), argument.ShortAliases); + if (argument.MemberName.StartsWith("Automatic")) + { + Assert.IsNull(argument.Member); + } + else + { + Assert.IsNotNull(argument.Member); + Assert.AreSame(argument.Parser.ArgumentsType.GetMember(argument.MemberName)[0], argument.Member); + } } private static void VerifyArguments(IEnumerable arguments, ExpectedArgument[] expected) @@ -1543,7 +1791,7 @@ public static IEnumerable ProviderKinds => new[] { new object[] { ProviderKind.Reflection }, - new object[] { ProviderKind.Generated } + [ProviderKind.Generated] }; public static void AssertSpanEqual(ReadOnlySpan expected, ReadOnlySpan actual) diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 80e33d1f..3ff4d225 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -#pragma warning disable OCL0033,OCL0034 +#pragma warning disable OCL0033,OCL0034,OCL0040 namespace Ookii.CommandLine.Tests; @@ -78,7 +78,7 @@ public int Run() // Not generated to test registration of plain commands without generation. [Command(IsHidden = true)] [Description("Async command description.")] -partial class AsyncCommand : IAsyncCommand +class AsyncCommand : IAsyncCommand { [CommandLineArgument(Position = 0)] [Description("Argument description.")] @@ -99,23 +99,16 @@ public Task RunAsync() [Command(IsHidden = true)] [Description("Async command description.")] -partial class AsyncCancelableCommand : IAsyncCancelableCommand +class AsyncCancelableCommand : AsyncCommandBase { [CommandLineArgument(Position = 0)] [Description("Argument description.")] public int Value { get; set; } - public int Run() - { - // Do something different than RunAsync so the test can differentiate which one was - // called. - return Value + 1; - } - - public async Task RunAsync(CancellationToken cancellationToken) + public override async Task RunAsync() { - await Task.Delay(Value, cancellationToken); - return 10; + await Task.Delay(Value, CancellationToken); + return Value; } } diff --git a/src/Ookii.CommandLine.Tests/CustomUsageWriter.cs b/src/Ookii.CommandLine.Tests/CustomUsageWriter.cs new file mode 100644 index 00000000..7cb5b619 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/CustomUsageWriter.cs @@ -0,0 +1,21 @@ +using System; + +namespace Ookii.CommandLine.Tests; + +internal class CustomUsageWriter : UsageWriter +{ + public CustomUsageWriter() { } + + public CustomUsageWriter(LineWrappingTextWriter writer) : base(writer) { } + + protected override void WriteParserUsageFooter() + { + WriteLine("This is a custom footer."); + } + + protected override void WriteCommandListUsageFooter() + { + base.WriteCommandListUsageFooter(); + WriteLine("This is the command list footer."); + } +} diff --git a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs index e2c5b0dd..ea0f4152 100644 --- a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs +++ b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs @@ -81,7 +81,7 @@ elementum curabitur. 0123456789012345678901234567890123456789012345678901234567890123456789012345 6789012345678901234567890123456789012345678901234567890123456789012345678901 234567890123456789012345678901234567890123456789 - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. @@ -123,6 +123,42 @@ elementum curabitur. 45678901234567890123456789012345678901234567890123456789 ".ReplaceLineEndings(); + private static readonly string _expectedIndentChangesNoMaximum = @" +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae elementum curabitur. + +Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae elementum curabitur. + +Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae elementum curabitur. + +Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +".ReplaceLineEndings(); + private static readonly string _expectedIndentNoMaximum = @" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. @@ -232,4 +268,107 @@ dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in 6789012345678901234567890123456789012345678901234567890123456789012345678901 234567890123456789012345678901234567890123456789".ReplaceLineEndings(); + private static readonly string _expectedIndentAfterEmptyLine = @" +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique + risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum + consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. + Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae + sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae + elementum curabitur. + +Lorem + 0123456789012345678901234567890123456789012345678901234567890123456789012345 + 6789012345678901234567890123456789012345678901234567890123456789012345678901 + 234567890123456789012345678901234567890123456789 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing + tristique risus nec feugiat in fermentum. + + Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum + consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. + Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae + sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae + elementum curabitur. + + Lorem + 0123456789012345678901234567890123456789012345678901234567890123456789012345 + 6789012345678901234567890123456789012345678901234567890123456789012345678901 + 234567890123456789012345678901234567890123456789 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique + risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum + consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. + Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae + sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae + elementum curabitur. + +Lorem + 0123456789012345678901234567890123456789012345678901234567890123456789012345 + 6789012345678901234567890123456789012345678901234567890123456789012345678901 + 234567890123456789012345678901234567890123456789 +".ReplaceLineEndings(); + + private static readonly string _expectedIndentAfterEmptyLineNoLimit = @" +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae elementum curabitur. + +Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. + + Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae elementum curabitur. + + Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. + +Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus + quam pellentesque nec + nam aliquam. Porta non pulvinar neque laoreet suspendisse interdum consectetur. + Arcu risus quis varius quam. Cursus mattis molestie a iaculis at erat. Malesuada fames ac turpis egestas maecenas pharetra. Fringilla est + ullamcorper eget nulla facilisi etiam dignissim diam. Condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus. + Augue neque gravida in + fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae elementum curabitur. + +Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +".ReplaceLineEndings(); + } diff --git a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs index 6ae81a9e..730c6635 100644 --- a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs +++ b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs @@ -137,6 +137,24 @@ public void TestIndentChanges() Assert.AreEqual(_expectedIndentChanges, writer.BaseWriter.ToString()); } + [TestMethod()] + public void TestIndentChangesNoMaximum() + { + using var writer = LineWrappingTextWriter.ForStringWriter(); + writer.Indent = 4; + writer.WriteLine(_input); + writer.Indent = 8; + writer.Write(_input.Trim()); + // Should add a new line. + writer.ResetIndent(); + writer.WriteLine(_input.Trim()); + // Should not add a new line. + writer.ResetIndent(); + writer.Flush(); + + Assert.AreEqual(_expectedIndentChangesNoMaximum, writer.BaseWriter.ToString()); + } + [TestMethod()] public async Task TestIndentChangesAsync() { @@ -408,8 +426,8 @@ public void TestToString() [TestMethod] public void TestWrappingMode() { + using (var writer = LineWrappingTextWriter.ForStringWriter(80)) { - using var writer = LineWrappingTextWriter.ForStringWriter(80); writer.Indent = 4; writer.WriteLine(_inputWrappingMode); writer.Wrapping = WrappingMode.Disabled; @@ -420,8 +438,8 @@ public void TestWrappingMode() } // Make sure the buffer is cleared if not empty. + using (var writer = LineWrappingTextWriter.ForStringWriter(80)) { - using var writer = LineWrappingTextWriter.ForStringWriter(80); writer.Indent = 4; writer.Write(_inputWrappingMode); writer.Wrapping = WrappingMode.Disabled; @@ -432,8 +450,8 @@ public void TestWrappingMode() } // Test EnabledNoForce + using (var writer = LineWrappingTextWriter.ForStringWriter(80)) { - using var writer = LineWrappingTextWriter.ForStringWriter(80); writer.Indent = 4; writer.Wrapping = WrappingMode.EnabledNoForce; writer.Write(_inputWrappingMode); @@ -443,15 +461,41 @@ public void TestWrappingMode() Assert.AreEqual(_expectedWrappingModeNoForce, writer.ToString()); } - // Should be false and unchangeable if no maximum length. + // Should be disabled and unchangeable if no maximum length. + using (var writer = LineWrappingTextWriter.ForStringWriter()) { - using var writer = LineWrappingTextWriter.ForStringWriter(); Assert.AreEqual(WrappingMode.Disabled, writer.Wrapping); writer.Wrapping = WrappingMode.Enabled; Assert.AreEqual(WrappingMode.Disabled, writer.Wrapping); } } + [TestMethod] + public void TestIndentAfterEmptyLine() + { + using var writer = LineWrappingTextWriter.ForStringWriter(80); + writer.Indent = 4; + writer.WriteLine(_input); + writer.IndentAfterEmptyLine = true; + writer.WriteLine(_input); + writer.IndentAfterEmptyLine = false; + writer.WriteLine(_input); + Assert.AreEqual(_expectedIndentAfterEmptyLine, writer.ToString()); + } + + [TestMethod] + public void TestIndentAfterEmptyLineNoLimit() + { + using var writer = LineWrappingTextWriter.ForStringWriter(); + writer.Indent = 4; + writer.WriteLine(_input); + writer.IndentAfterEmptyLine = true; + writer.WriteLine(_input); + writer.IndentAfterEmptyLine = false; + writer.WriteLine(_input); + Assert.AreEqual(_expectedIndentAfterEmptyLineNoLimit, writer.ToString()); + } + [TestMethod] public void TestExactLineLength() { diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index f1b41bd0..30d5a680 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -15,8 +15,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Ookii.CommandLine.Tests/StandardStreamTest.cs b/src/Ookii.CommandLine.Tests/StandardStreamTest.cs new file mode 100644 index 00000000..b6c1defd --- /dev/null +++ b/src/Ookii.CommandLine.Tests/StandardStreamTest.cs @@ -0,0 +1,56 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Terminal; +using System; +using System.IO; + +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class StandardStreamTest +{ + [TestMethod] + public void TestGetWriter() + { + Assert.AreSame(Console.Out, StandardStream.Output.GetWriter()); + Assert.AreSame(Console.Error, StandardStream.Error.GetWriter()); + Assert.ThrowsException(() => StandardStream.Input.GetWriter()); + } + + [TestMethod] + public void TestOpenStream() + { + using var output = StandardStream.Output.OpenStream(); + using var error = StandardStream.Error.OpenStream(); + using var input = StandardStream.Input.OpenStream(); + Assert.AreNotSame(output, input); + Assert.AreNotSame(output, error); + Assert.AreNotSame(error, input); + } + + [TestMethod] + public void TestGetStandardStream() + { + Assert.AreEqual(StandardStream.Output, Console.Out.GetStandardStream()); + Assert.AreEqual(StandardStream.Error, Console.Error.GetStandardStream()); + Assert.AreEqual(StandardStream.Input, Console.In.GetStandardStream()); + using (var writer = new StringWriter()) + { + Assert.IsNull(writer.GetStandardStream()); + } + + using (var writer = LineWrappingTextWriter.ForConsoleOut()) + { + Assert.AreEqual(StandardStream.Output, writer.GetStandardStream()); + } + + using (var writer = LineWrappingTextWriter.ForStringWriter()) + { + Assert.IsNull(writer.GetStandardStream()); + } + + using (var reader = new StringReader("foo")) + { + Assert.IsNull(reader.GetStandardStream()); + } + } +} diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs index f527172c..5e19fa11 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs @@ -151,5 +151,23 @@ Other parent command description. -Help [] (-?, -h) Displays this help message. +".ReplaceLineEndings(); + + public static readonly string _expectedUsageFooter = @"Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + custom + Custom parsing command. + + test + Test command description. + + version + Displays version information. + +This is the command list footer. ".ReplaceLineEndings(); } diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 6de3875c..f020d272 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -251,6 +251,25 @@ public void TestWriteUsageApplicationDescription(ProviderKind kind) Assert.AreEqual(_expectedUsageWithDescription, writer.BaseWriter.ToString()); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageFooter(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() + { + Error = writer, + UsageWriter = new CustomUsageWriter(writer) + { + ExecutableName = _executableName, + } + }; + + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageFooter, writer.BaseWriter.ToString()); + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestCommandUsage(ProviderKind kind) @@ -355,9 +374,13 @@ await Assert.ThrowsExceptionAsync( async () => await manager.RunCommandAsync(["AsyncCancelableCommand", "10000"], source.Token)); // Command works if not passed a token. - Assert.AreEqual(10, await manager.RunCommandAsync(["AsyncCancelableCommand", "10"])); - } + var result = await manager.RunCommandAsync(["AsyncCancelableCommand", "10"]); + Assert.AreEqual(10, result); + // RunCommand works but calls Run. + result = manager.RunCommand(["AsyncCancelableCommand", "5"]); + Assert.AreEqual(5, result); + } [TestMethod] public async Task TestAsyncCommandBase() diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index fe1df5fa..903df1f1 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -185,6 +185,8 @@ public HelpArgument(CommandLineParser parser, string argumentName, char shortNam { } + public override MemberInfo? Member => null; + protected override bool CanSetProperty => false; private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName, char shortName, char shortAlias) @@ -242,6 +244,8 @@ public VersionArgument(CommandLineParser parser, string argumentName) { } + public override MemberInfo? Member => null; + protected override bool CanSetProperty => false; private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName) @@ -292,6 +296,7 @@ internal struct ArgumentInfo public bool IsRequiredProperty { get; set; } public object? DefaultValue { get; set; } public bool IncludeDefaultValueInHelp { get; set; } + public string? DefaultValueFormat { get; set; } public string? Description { get; set; } public string? ValueDescription { get; set; } public bool AllowNull { get; set; } @@ -316,7 +321,6 @@ internal struct ArgumentInfo private readonly Type _elementTypeWithNullable; private readonly string? _description; private readonly bool _isRequired; - private readonly string _memberName; private readonly object? _defaultValue; private readonly ArgumentKind _argumentKind; private readonly bool _allowNull; @@ -331,7 +335,7 @@ internal CommandLineArgument(ArgumentInfo info) { // If this method throws anything other than a NotSupportedException, it constitutes a bug in the Ookii.CommandLine library. _parser = info.Parser; - _memberName = info.MemberName; + MemberName = info.MemberName; _argumentName = info.ArgumentName; if (_parser.Mode == ParsingMode.LongShort) { @@ -385,6 +389,7 @@ internal CommandLineArgument(ArgumentInfo info) _converter = info.Converter; _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); IncludeDefaultInUsageHelp = info.IncludeDefaultValueInHelp; + DefaultValueFormat = info.DefaultValueFormat; _valueDescription = info.ValueDescription; _allowNull = info.AllowNull; DictionaryInfo = info.DictionaryInfo; @@ -409,10 +414,16 @@ internal CommandLineArgument(ArgumentInfo info) /// /// The name of the property or method that defined this command line argument. /// - public string MemberName - { - get { return _memberName; } - } + public string MemberName { get; } + + /// + /// Gets the for the member that defined this argument. + /// + /// + /// An instance of the or class, or + /// if this is the automatic version or help argument. + /// + public abstract MemberInfo? Member { get; } /// /// Gets the name of this argument. @@ -687,6 +698,19 @@ public object? DefaultValue get { return _defaultValue; } } + /// + /// Gets the compound formatting string that is used to format the default value for display in + /// the usage help. + /// + /// + /// A compound formatting string, or if the default format is used. + /// + /// +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.CompositeFormat)] +#endif + public string? DefaultValueFormat { get; } + /// /// Gets a value that indicates whether the default value should be included in the argument's /// description in the usage help. @@ -817,7 +841,7 @@ public string Description /// /// /// - /// For dictionary arguments, this property only returns the information that apples to both + /// For dictionary arguments, this property only returns the information that applies to both /// dictionary and multi-value arguments. For information that applies to dictionary /// arguments, but not other types of multi-value arguments, use the /// property. @@ -1165,6 +1189,7 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), DefaultValue = attribute.DefaultValue, IncludeDefaultValueInHelp = attribute.IncludeDefaultInUsageHelp, + DefaultValueFormat = attribute.DefaultValueFormat, IsRequired = attribute.IsRequired || requiredProperty, IsRequiredProperty = requiredProperty, MemberName = memberName, diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index e5e9e808..26dece2e 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -1,5 +1,6 @@ using Ookii.CommandLine.Commands; using System; +using System.Diagnostics.CodeAnalysis; namespace Ookii.CommandLine; @@ -315,6 +316,24 @@ public bool IsPositional /// public bool IncludeDefaultInUsageHelp { get; set; } = true; + /// + /// Gets or sets a compound formatting string that is used to format the default value for + /// display in the usage help. + /// + /// + /// A compound formatting string, or to use the default format. + /// + /// + /// + /// This value must be a compound formatting string containing exactly one placeholder, + /// e.g "0x{0:x}". + /// + /// +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.CompositeFormat)] +#endif + public string? DefaultValueFormat { get; set; } + /// /// Gets or sets a value that indicates whether argument parsing should be canceled if /// this argument is encountered. diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 8cceac9f..40fea575 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -183,6 +184,47 @@ private struct PrefixInfo public bool Short { get; set; } } + private struct ParseState + { + public CommandLineParser Parser; + + public ReadOnlyMemory Arguments; + + public int Index; + + public int PositionalArgumentIndex; + + public bool PositionalOnly; + + public CancelMode CancelParsing; + + public CommandLineArgument? Argument; + + public ReadOnlyMemory ArgumentName; + + public ReadOnlyMemory? ArgumentValue; + + public bool IsUnknown; + + public bool IsSpecifiedByPosition; + + public readonly CommandLineArgument? PositionalArgument + => PositionalArgumentIndex < Parser._positionalArgumentCount ? Parser.Arguments[PositionalArgumentIndex] : null; + + public readonly string RealArgumentName => Argument?.ArgumentName ?? ArgumentName.ToString(); + + public readonly ReadOnlyMemory RemainingArguments => Arguments.Slice(Index + 1); + + public void ResetForNextArgument() + { + Argument = null; + ArgumentName = default; + ArgumentValue = null; + IsUnknown = false; + IsSpecifiedByPosition = false; + } + } + #endregion private readonly ArgumentProvider _provider; @@ -252,12 +294,36 @@ private struct PrefixInfo /// arguments. /// /// - /// This even is only raised when the property is + /// This event is only raised when the property is /// . /// /// public event EventHandler? DuplicateArgument; + /// + /// Event raised when an unknown argument name or a positional value with no matching argument + /// is used. + /// + /// + /// + /// Specifying an argument with an unknown name, or too many positional arguments, is normally + /// an error. By handling this event and setting the + /// property to + /// , you can instead continue parsing the remainder of the command + /// line, ignoring the unknown argument. + /// + /// + /// You can also cancel parsing instead using the + /// property. + /// + /// + /// If an unknown argument name is encountered and is followed by a value separated by + /// whitespace, that value will be treated as the next positional argument value. It is not + /// considered to be a value for the unknown argument. + /// + /// + public event EventHandler? UnknownArgument; + internal const string UnreferencedCodeHelpUrl = "https://www.ookii.org/Link/CommandLineSourceGeneration"; /// @@ -360,7 +426,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null _argumentNamePrefixes = DetermineArgumentNamePrefixes(_parseOptions); _nameValueSeparators = DetermineNameValueSeparators(_parseOptions); var prefixInfos = _argumentNamePrefixes.Select(p => new PrefixInfo { Prefix = p, Short = true }); - if (_mode == ParsingMode.LongShort) + if (_mode == ParsingMode.LongShort || _parseOptions.PrefixTerminationOrDefault != PrefixTerminationMode.None) { _longArgumentNamePrefix = _parseOptions.LongArgumentNamePrefixOrDefault; if (string.IsNullOrWhiteSpace(_longArgumentNamePrefix)) @@ -368,9 +434,12 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); } - var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; - prefixInfos = prefixInfos.Append(longInfo); - _argumentsByShortName = new(new CharComparer(comparison)); + if (_mode == ParsingMode.LongShort) + { + var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; + prefixInfos = prefixInfos.Append(longInfo); + _argumentsByShortName = new(new CharComparer(comparison)); + } } _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); @@ -431,14 +500,18 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// The prefix for long argument names, or if the - /// property is not . + /// property is not and the + /// property is + /// . /// /// /// /// The long argument prefix is only used if the property is - /// . See to - /// get the prefixes for short argument names, or for argument names if the - /// property is . + /// , or if the + /// property is not + /// . See to + /// get the prefixes for short argument names, or for all argument names if the + /// property is . /// /// /// @@ -451,6 +524,9 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// The that was used to define the arguments. /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] +#endif public Type ArgumentsType => _provider.ArgumentsType; /// @@ -477,17 +553,32 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// Gets a description that is used when generating usage information. /// /// - /// The description of the command line application. The default value is an empty string (""). + /// The description of the command line application. /// /// /// - /// If not empty, this description will be added to the usage returned by the - /// method. This description can be set by applying the to - /// the command line arguments type. + /// If not empty, this description will be added at the top of the usage help created by the + /// method. This description can be set by applying the + /// attribute to the command line arguments class. /// /// public string Description => _provider.Description; + /// + /// Gets footer text that is used when generating usage information. + /// + /// + /// The footer text. + /// + /// + /// + /// If not empty, this footer will be added at the bottom of the usage help created by the + /// method. This footer can be set by applying the + /// attribute to the command line arguments class. + /// + /// + public string UsageFooter => _provider.UsageFooter; + /// /// Gets the options used by this instance. /// @@ -912,16 +1003,21 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// The command line arguments. public object? Parse(ReadOnlyMemory args) { - int index = -1; + var state = new ParseState() + { + Parser = this, + Arguments = args, + }; + try { HelpRequested = false; - return ParseCore(args, ref index); + return ParseCore(ref state); } catch (CommandLineArgumentException ex) { HelpRequested = true; - ParseResult = ParseResult.FromException(ex, args.Slice(index)); + ParseResult = ParseResult.FromException(ex, args.Slice(state.Index)); throw; } } @@ -1270,55 +1366,41 @@ public static ImmutableArray GetDefaultArgumentNamePrefixes() /// Raises the event. /// /// The data for the event. - protected virtual void OnArgumentParsed(ArgumentParsedEventArgs e) - { - ArgumentParsed?.Invoke(this, e); - } + protected virtual void OnArgumentParsed(ArgumentParsedEventArgs e) => ArgumentParsed?.Invoke(this, e); /// /// Raises the event. /// /// The data for the event. - protected virtual void OnDuplicateArgument(DuplicateArgumentEventArgs e) - { - DuplicateArgument?.Invoke(this, e); - } + protected virtual void OnDuplicateArgument(DuplicateArgumentEventArgs e) => DuplicateArgument?.Invoke(this, e); - internal static bool ShouldIndent(LineWrappingTextWriter writer) - { - return writer.MaximumLineLength is 0 or >= 30; - } + /// + /// Raises the event. + /// + /// The data for the event. + protected virtual void OnUnknownArgument(UnknownArgumentEventArgs e) => UnknownArgument?.Invoke(this, e); + + internal static bool ShouldIndent(LineWrappingTextWriter writer) => writer.MaximumLineLength is 0 or >= 30; internal static void WriteError(ParseOptions options, string message, TextFormat color, bool blankLine = false) { using var errorVtSupport = options.EnableErrorColor(); - try + using var error = DisposableWrapper.Create(options.Error, LineWrappingTextWriter.ForConsoleError); + if (errorVtSupport.IsSupported) { - using var error = DisposableWrapper.Create(options.Error, LineWrappingTextWriter.ForConsoleError); - if (options.UseErrorColor ?? false) - { - error.Inner.Write(color); - } - - error.Inner.Write(message); - if (options.UseErrorColor ?? false) - { - error.Inner.Write(options.UsageWriter.ColorReset); - } + error.Inner.Write(color); + } - error.Inner.WriteLine(); - if (blankLine) - { - error.Inner.WriteLine(); - } + error.Inner.Write(message); + if (errorVtSupport.IsSupported) + { + error.Inner.Write(options.UsageWriter.ColorReset); } - finally + + error.Inner.WriteLine(); + if (blankLine) { - // Reset UseErrorColor if it was changed. - if (errorVtSupport != null) - { - options.UseErrorColor = null; - } + error.Inner.WriteLine(); } } @@ -1476,62 +1558,56 @@ private void VerifyPositionalArgumentRules() } } - private object? ParseCore(ReadOnlyMemory args, ref int x) + private object? ParseCore(ref ParseState state) { - // Reset all arguments to their default value. - foreach (CommandLineArgument argument in _arguments) + Reset(); + for (; state.Index < state.Arguments.Length; ++state.Index) { - argument.Reset(); - } - - HelpRequested = false; - int positionalArgumentIndex = 0; - - var cancelParsing = CancelMode.None; - CommandLineArgument? lastArgument = null; - for (x = 0; x < args.Length; ++x) - { - string arg = args.Span[x]; - var argumentNamePrefix = CheckArgumentNamePrefix(arg); - if (argumentNamePrefix != null) + var token = state.Arguments.Span[state.Index]; + state.ResetForNextArgument(); + if (!state.PositionalOnly && token == _longArgumentNamePrefix) { - // If white space was the value separator, this function returns the index of argument containing the value for the named argument. - // It returns -1 if parsing was canceled by the ArgumentParsed event handler or the CancelParsing property. - (cancelParsing, x, lastArgument) = ParseNamedArgument(args.Span, x, argumentNamePrefix.Value); - if (cancelParsing != CancelMode.None) + if (_parseOptions.PrefixTerminationOrDefault == PrefixTerminationMode.PositionalOnly) + { + state.PositionalOnly = true; + continue; + } + else if (_parseOptions.PrefixTerminationOrDefault == PrefixTerminationMode.CancelWithSuccess) { + state.CancelParsing = CancelMode.Success; + state.ArgumentName = default; break; } } - else + + if (state.PositionalOnly || !FindNamedArgument(token, ref state)) { - // If this is a multi-value argument is must be the last argument. - if (positionalArgumentIndex < _positionalArgumentCount && _arguments[positionalArgumentIndex].MultiValueInfo == null) - { - // Skip named positional arguments that have already been specified by name. - while (positionalArgumentIndex < _positionalArgumentCount && _arguments[positionalArgumentIndex].MultiValueInfo == null && _arguments[positionalArgumentIndex].HasValue) - { - ++positionalArgumentIndex; - } - } + state.IsSpecifiedByPosition = true; + state.ArgumentValue = token.AsMemory(); + FindPositionalArgument(ref state); + } - if (positionalArgumentIndex >= _positionalArgumentCount) - { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); - } + if (state.IsUnknown) + { + HandleUnknownArgument(ref state); + } - lastArgument = _arguments[positionalArgumentIndex]; - cancelParsing = ParseArgumentValue(lastArgument, arg, arg.AsMemory()); - if (cancelParsing != CancelMode.None) - { - break; - } + // Argument can be null without IsUnknown set if the token was a combined short switch + // argument. + if (state.Argument != null) + { + ParseArgumentValue(ref state); + } + + if (state.CancelParsing != CancelMode.None) + { + break; } } - if (cancelParsing == CancelMode.Abort) + if (state.CancelParsing == CancelMode.Abort) { - ParseResult = ParseResult.FromCanceled(lastArgument!.ArgumentName, args.Slice(x + 1)); + ParseResult = ParseResult.FromCanceled(state.RealArgumentName, state.RemainingArguments); return null; } @@ -1544,6 +1620,21 @@ private void VerifyPositionalArgumentRules() // Run class validators. _provider.RunValidators(this); + var result = CreateResultInstance(); + + ParseResult = state.CancelParsing == CancelMode.None + ? ParseResult.FromSuccess() + : ParseResult.FromSuccess(state.Argument?.ArgumentName ?? + (state.ArgumentName.Length == 0 ? LongArgumentNamePrefix : state.ArgumentName.ToString()), + state.RemainingArguments); + + // Reset to false in case it was set by a method argument that didn't cancel parsing. + HelpRequested = false; + return result; + } + + private object CreateResultInstance() + { object commandLineArguments; try { @@ -1574,13 +1665,61 @@ private void VerifyPositionalArgumentRules() argument.ApplyPropertyValue(commandLineArguments); } - ParseResult = cancelParsing == CancelMode.None - ? ParseResult.FromSuccess() - : ParseResult.FromSuccess(lastArgument!.ArgumentName, args.Slice(x + 1)); + return commandLineArguments; + } - // Reset to false in case it was set by a method argument that didn't cancel parsing. + private void Reset() + { HelpRequested = false; - return commandLineArguments; + + // Reset all arguments to their default value, and mark them as unassigned. + foreach (var argument in _arguments) + { + argument.Reset(); + } + } + + private void ParseArgumentValue(ref ParseState state) + { + Debug.Assert(state.Argument != null); + + var argument = state.Argument!; + bool parsedValue = false; + if (state.ArgumentValue == null && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) + { + // No value separator was present in the token, but a value is required and white space is + // allowed. We take the next token as the value. For multi-value arguments that can consume + // multiple tokens, we keep going until we hit another argument name. + var allowMultiToken = argument.MultiValueInfo is MultiValueArgumentInfo info + && (info.AllowWhiteSpaceSeparator || state.IsSpecifiedByPosition); + + int index; + for (index = state.Index + 1; index < state.Arguments.Length; ++index) + { + var stringValue = state.Arguments.Span[index]; + if (CheckArgumentNamePrefix(stringValue) != null) + { + --index; + break; + } + + parsedValue = true; + state.CancelParsing = ParseArgumentValue(argument, stringValue, stringValue.AsMemory()); + if (state.CancelParsing != CancelMode.None || !allowMultiToken) + { + break; + } + } + + state.Index = index; + } + + // If the value was not parsed above, parse it now. In case there is no value and it's + // not a switch, CommandLineArgument.SetValue will throw an exception. + if (!parsedValue) + { + state.CancelParsing = ParseArgumentValue(argument, null, state.ArgumentValue); + } } private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) @@ -1629,74 +1768,67 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri return e.CancelParsing; } - private (CancelMode, int, CommandLineArgument?) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) + private static void FindPositionalArgument(ref ParseState state) { - var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitFirstOfAny(_nameValueSeparators.AsSpan()); + // Skip named positional arguments that have already been specified by name, unless it's + // a multi-value argument which must be the last positional argument. + while (state.PositionalArgument is CommandLineArgument current && current.MultiValueInfo == null && current.HasValue) + { + ++state.PositionalArgumentIndex; + } - CancelMode cancelParsing; - CommandLineArgument? argument = null; - if (_argumentsByShortName != null && prefix.Short) + state.Argument = state.PositionalArgument; + if (state.Argument == null) { - if (argumentName.Length == 1) - { - argument = GetShortArgumentOrThrow(argumentName.Span[0]); - } - else - { - CommandLineArgument? lastArgument; - (cancelParsing, lastArgument) = ParseShortArgument(argumentName.Span, argumentValue); - return (cancelParsing, index, lastArgument); - } + state.IsUnknown = true; + return; } - if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) + state.ArgumentName = state.Argument.ArgumentName.AsMemory(); + } + + private bool FindNamedArgument(string token, ref ParseState state) + { + if (CheckArgumentNamePrefix(token) is not PrefixInfo prefix) { - if (Options.AutoPrefixAliasesOrDefault) + return false; + } + + (state.ArgumentName, state.ArgumentValue) = + token.AsMemory(prefix.Prefix.Length).SplitFirstOfAny(_nameValueSeparators.AsSpan()); + + if (_argumentsByShortName != null && prefix.Short) + { + if (state.ArgumentName.Length == 1) { - argument = GetArgumentByNamePrefix(argumentName.Span); + state.Argument = GetShortArgument(state.ArgumentName.Span[0]); + state.IsUnknown = state.Argument == null; + return true; } - - if (argument == null) + else { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); + ParseCombinedShortArgument(ref state); + return true; } } - argument.SetUsedArgumentName(argumentName); - if (!argumentValue.HasValue && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) + if (state.Argument == null && !_argumentsByName.TryGetValue(state.ArgumentName, out state.Argument)) { - string? argumentValueString = null; - - // No separator was present but a value is required. We take the next argument as - // its value. For multi-value arguments that can consume multiple values, we keep - // going until we hit another argument name. - while (index + 1 < args.Length && CheckArgumentNamePrefix(args[index + 1]) == null) + if (Options.AutoPrefixAliasesOrDefault) { - ++index; - argumentValueString = args[index]; - - cancelParsing = ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory()); - if (cancelParsing != CancelMode.None) - { - return (cancelParsing, index, argument); - } - - if (argument.MultiValueInfo is not MultiValueArgumentInfo { AllowWhiteSpaceSeparator: true }) - { - break; - } + state.Argument = GetArgumentByNamePrefix(state.ArgumentName.Span); } - if (argumentValueString != null) + if (state.Argument == null) { - return (CancelMode.None, index, argument); + state.IsUnknown = true; + return true; } } - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler - // or the CancelParsing property. - cancelParsing = ParseArgumentValue(argument, null, argumentValue); - return (cancelParsing, index, argument); + state.Argument.SetUsedArgumentName(state.ArgumentName); + state.ArgumentName = state.Argument.ArgumentName.AsMemory(); + return true; } private CommandLineArgument? GetArgumentByNamePrefix(ReadOnlySpan prefix) @@ -1738,40 +1870,63 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri return foundArgument; } - private (CancelMode, CommandLineArgument?) ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) + private void ParseCombinedShortArgument(ref ParseState state) { - CommandLineArgument? arg = null; - foreach (var ch in name) + var combinedName = state.ArgumentName.Span; + foreach (var ch in combinedName) { - arg = GetShortArgumentOrThrow(ch); - if (!arg.IsSwitch) + var argument = GetShortArgument(ch); + if (argument == null) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); + state.ArgumentName = ch.ToString().AsMemory(); + HandleUnknownArgument(ref state, true); + continue; } - var cancelParsing = ParseArgumentValue(arg, null, value); - if (cancelParsing != CancelMode.None) + if (!argument.IsSwitch) { - return (cancelParsing, arg); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, + combinedName.ToString()); } - } - return (CancelMode.None, arg); + state.ArgumentName = argument.ArgumentName.AsMemory(); + state.CancelParsing = ParseArgumentValue(argument, null, state.ArgumentValue); + if (state.CancelParsing != CancelMode.None) + { + break; + } + } } - private CommandLineArgument GetShortArgumentOrThrow(char shortName) + private void HandleUnknownArgument(ref ParseState state, bool isCombined = false) { - if (_argumentsByShortName!.TryGetValue(shortName, out CommandLineArgument? argument)) + var eventArgs = new UnknownArgumentEventArgs(state.Arguments.Span[state.Index], state.ArgumentName, + state.ArgumentValue ?? default, isCombined); + + OnUnknownArgument(eventArgs); + if (eventArgs.CancelParsing != CancelMode.None) { - return argument; + state.CancelParsing = eventArgs.CancelParsing; + return; } - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, shortName.ToString()); + if (!eventArgs.Ignore) + { + if (state.ArgumentName.Length > 0) + { + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, + state.ArgumentName.ToString()); + } + + + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); + } } private PrefixInfo? CheckArgumentNamePrefix(string argument) { - // Even if '-' is the argument name prefix, we consider an argument starting with dash followed by a digit as a value, because it could be a negative number. + // Even if '-' is the argument name prefix, we consider an argument starting with dash + // followed by a digit as a value, because it could be a negative number. if (argument.Length >= 2 && argument[0] == '-' && char.IsDigit(argument, 1)) { return null; diff --git a/src/Ookii.CommandLine/Commands/AsyncCancelableCommandBase.cs b/src/Ookii.CommandLine/Commands/AsyncCancelableCommandBase.cs deleted file mode 100644 index 7a4995d4..00000000 --- a/src/Ookii.CommandLine/Commands/AsyncCancelableCommandBase.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Ookii.CommandLine.Commands; - -/// -/// Base class for asynchronous commands with cancellation support that want the -/// method to invoke the -/// method. -/// -/// -/// -/// This class is provided for convenience for creating asynchronous commands without having to -/// implement the method. -/// -/// -/// This class implements the interface, which can use the -/// cancellation token passed to the -/// method. -/// -/// -/// If you do not need the cancellation token, you can implement the -/// interface or derive from the class instead. -/// -/// -/// -public abstract class AsyncCancelableCommandBase : IAsyncCancelableCommand -{ - /// - /// Calls the method and waits synchronously for it to complete. - /// - /// The exit code of the command. - public virtual int Run() => Task.Run(() => RunAsync(default)).ConfigureAwait(false).GetAwaiter().GetResult(); - - /// - public abstract Task RunAsync(CancellationToken token); -} diff --git a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs index 96dc75f2..d40e39eb 100644 --- a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs +++ b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace Ookii.CommandLine.Commands; @@ -11,16 +12,13 @@ namespace Ookii.CommandLine.Commands; /// This class is provided for convenience for creating asynchronous commands without having to /// implement the method. /// -/// -/// If you want to use the cancellation token passed to the -/// -/// method, you should instead implement the interface or -/// derive from the class. -/// /// /// -public abstract class AsyncCommandBase : IAsyncCommand +public abstract class AsyncCommandBase : IAsyncCancelableCommand { + /// + public CancellationToken CancellationToken { get; set; } + /// /// Calls the method and waits synchronously for it to complete. /// diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index d8fc0558..c745995d 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -37,8 +37,8 @@ namespace Ookii.CommandLine.Commands; /// interface. /// /// -/// Subcommands can be asynchronous by implementing the or -/// interface. +/// Subcommands can support asynchronous execution by implementing the +/// or interface. /// /// /// Commands can be defined in a single assembly, or in multiple assemblies. @@ -735,8 +735,7 @@ public IEnumerable GetCommands() /// /// /// A task representing the asynchronous run operation. The result is the value returned - /// by , - /// , + /// by or /// , or if the command /// could not be created. /// @@ -744,10 +743,10 @@ public IEnumerable GetCommands() /// /// This function creates the command by invoking the /// method. If the command implements the interface, it - /// invokes the method; if - /// the command implements the interface, it invokes the - /// method; otherwise, it invokes the - /// method on the command. + /// sets the + /// property. If the command implements the interface, it invokes + /// the method; otherwise, it invokes + /// the method on the command. /// /// /// Commands that don't meet the criteria of the @@ -788,8 +787,7 @@ public IEnumerable GetCommands() /// /// /// A task representing the asynchronous run operation. The result is the value returned - /// by , - /// , + /// by or /// , or if the command /// could not be created. /// @@ -797,10 +795,10 @@ public IEnumerable GetCommands() /// /// This function creates the command by invoking the /// method. If the command implements the interface, it - /// invokes the method; if - /// the command implements the interface, it invokes the - /// method; otherwise, it invokes the - /// method on the command. + /// sets the + /// property. If the command implements the interface, it invokes + /// the method; otherwise, it invokes + /// the method on the command. /// /// /// Commands that don't meet the criteria of the @@ -843,8 +841,7 @@ public IEnumerable GetCommands() /// /// /// A task representing the asynchronous run operation. The result is the value returned - /// by , - /// , + /// by or /// , or if the command /// could not be created. /// @@ -852,10 +849,10 @@ public IEnumerable GetCommands() /// /// This function creates the command by invoking the /// method. If the command implements the interface, it - /// invokes the method; if - /// the command implements the interface, it invokes the - /// method; otherwise, it invokes the - /// method on the command. + /// sets the + /// property. If the command implements the interface, it invokes + /// the method; otherwise, it invokes + /// the method on the command. /// /// /// Commands that don't meet the criteria of the @@ -898,8 +895,7 @@ public IEnumerable GetCommands() /// /// /// A task representing the asynchronous run operation. The result is the value returned - /// by , - /// , + /// by or /// , or if the command /// could not be created. /// @@ -907,10 +903,10 @@ public IEnumerable GetCommands() /// /// This function creates the command by invoking the /// method. If the command implements the interface, it - /// invokes the method; if - /// the command implements the interface, it invokes the - /// method; otherwise, it invokes the - /// method on the command. + /// sets the + /// property. If the command implements the interface, it invokes + /// the method; otherwise, it invokes + /// the method on the command. /// /// /// Commands that don't meet the criteria of the @@ -955,10 +951,10 @@ public IEnumerable GetCommands() /// /// This function creates the command by invoking the /// method. If the command implements the interface, it - /// invokes the method; if - /// the command implements the interface, it invokes the - /// method; otherwise, it invokes the - /// method on the command. + /// sets the + /// property. If the command implements the interface, it invokes + /// the method; otherwise, it invokes + /// the method on the command. /// /// /// Commands that don't meet the criteria of the @@ -1087,11 +1083,16 @@ private IEnumerable GetCommandsUnsortedAndFiltered() private static async Task RunCommandAsync(ICommand? command, CancellationToken cancellationToken) { - return command switch + if (command is IAsyncCancelableCommand asyncCancelableCommand) { - IAsyncCancelableCommand asyncCancelableCommand => await asyncCancelableCommand.RunAsync(cancellationToken), - IAsyncCommand asyncCommand => await asyncCommand.RunAsync(), - _ => command?.Run() - }; + asyncCancelableCommand.CancellationToken = cancellationToken; + return await asyncCancelableCommand.RunAsync(); + } + else if (command is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); + } + + return command?.Run(); } } diff --git a/src/Ookii.CommandLine/Commands/IAsyncCancelableCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCancelableCommand.cs index ac7b9468..0e94fdd5 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCancelableCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCancelableCommand.cs @@ -8,48 +8,35 @@ namespace Ookii.CommandLine.Commands; /// /// /// -/// This interface adds a method to the -/// interface, that will be invoked by the -/// method and its overloads. This allows you to write tasks that use asynchronous code. This -/// method receives the that was passed to the -/// method. +/// This interface adds a property to the +/// interface. The +/// method +/// and its overloads will set this property prior to calling the +/// method. /// /// -/// Use the class as a base class for your command to get -/// a default implementation of the -/// -/// -/// If you do not need the cancellation token, you can implement the -/// interface or derive from the class instead. +/// Use the class as a base class for your command to get a default +/// implementation of the method and the property. /// /// -public interface IAsyncCancelableCommand : ICommand +public interface IAsyncCancelableCommand : IAsyncCommand { /// - /// Runs the command asynchronously. - /// - /// - /// The cancellation token that was passed to the - /// + /// Gets or sets the cancellation token that can be used by the /// method. - /// - /// - /// A task that represents the asynchronous run operation. The result of the task is the - /// exit code for the command. - /// + /// + /// + /// A instance, or + /// if none was set. + /// /// /// - /// Typically, your application's Main() method should return the exit code of the - /// command that was executed. - /// - /// - /// This method will only be invoked if you run commands with the - /// - /// method or one of its overloads. Typically, it's recommended to implement the - /// method to invoke this method and wait for - /// it. Use the class for a default implementation - /// that does this. + /// If a was passed to the + /// method, + /// this property will be set to that token prior to the + /// method being called. /// /// - Task RunAsync(CancellationToken cancellationToken); + public CancellationToken CancellationToken { get; set; } } diff --git a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index 419f4d13..c9d991fa 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -18,8 +18,8 @@ namespace Ookii.CommandLine.Commands; /// /// If you want to use the cancellation token passed to the /// -/// method, you should instead implement the interface or -/// derive from the class. +/// method, you should instead implement the interface, or +/// derive from the class. /// /// public interface IAsyncCommand : ICommand diff --git a/src/Ookii.CommandLine/Conversion/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs index d51f9a53..f1700de3 100644 --- a/src/Ookii.CommandLine/Conversion/EnumConverter.cs +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -1,5 +1,7 @@ -using System; +using Ookii.CommandLine.Validation; +using System; using System.Globalization; +using System.Linq; namespace Ookii.CommandLine.Conversion; @@ -9,18 +11,27 @@ namespace Ookii.CommandLine.Conversion; /// /// /// This converter performs a case insensitive conversion, and accepts the name of an enumeration -/// value, or its underlying value. In the latter case, the value does not need to be one of the -/// defined values of the enumeration; use the -/// attribute to ensure only defined enumeration values can be used. +/// value or a number representing the underlying type of the enumeration. Comma-separated values +/// that will be combined using bitwise-or are also accepted, regardless of whether the +/// enumeration uses the attribute. When using a numeric value, the +/// value does not need to be one of the defined values of the enumeration. /// /// -/// A comma-separated list of values is also accepted, which will be combined using a bitwise-or -/// operation. This is accepted regardless of whether the enumeration uses the -/// attribute. +/// Use the attribute to alter these behaviors. Applying +/// that attribute will ensure that only values defined by the enumeration are allowed. The +/// +/// property can be used to control the use of multiple values, and the +/// property +/// controls the use of numbers instead of names. Set the +/// property to +/// to enable case sensitive conversion. /// /// -/// If conversion fails, this converter will provide an error message that includes all the -/// allowed values for the enumeration. +/// If conversion fails, the converter will check the +/// +/// property to see whether or not the enumeration's defined values should be listed in the +/// error message. If the argument does not have the +/// attribute applied, the enumeration's values will be listed in the message. /// /// /// @@ -75,22 +86,9 @@ public EnumConverter(Type enumType) /// The value was not valid for the enumeration type. /// public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) - { - try - { - return Enum.Parse(EnumType, value, true); - } - catch (ArgumentException ex) - { - throw CreateException(value, ex, argument); - } - catch (OverflowException ex) - { - throw CreateException(value, ex, argument); - } - } - #if NET6_0_OR_GREATER + => Convert(value.AsSpan(), culture, argument); + /// /// Converts a string span to the enumeration type. /// @@ -110,25 +108,38 @@ public EnumConverter(Type enumType) /// The value was not valid for the enumeration type. /// public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) +#endif { + var attribute = argument.Validators.OfType().FirstOrDefault(); +#if NET6_0_OR_GREATER + if (attribute != null && !attribute.ValidateBeforeConversion(argument, value)) +#else + if (attribute != null && !attribute.ValidateBeforeConversion(argument, value.AsSpan())) +#endif + { + throw CreateException(value.ToString(), null, argument, attribute); + } + try { - return Enum.Parse(EnumType, value, true); + return Enum.Parse(EnumType, value, !attribute?.CaseSensitive ?? true); } catch (ArgumentException ex) { - throw CreateException(value.ToString(), ex, argument); + throw CreateException(value.ToString(), ex, argument, attribute); } catch (OverflowException ex) { - throw CreateException(value.ToString(), ex, argument); + throw CreateException(value.ToString(), ex, argument, attribute); } } -#endif - private Exception CreateException(string value, Exception inner, CommandLineArgument argument) + private CommandLineArgumentException CreateException(string value, Exception? inner, CommandLineArgument argument, + ValidateEnumValueAttribute? attribute) { - var message = argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, EnumType, value, true); - return new CommandLineArgumentException(message, argument.ArgumentName, CommandLineArgumentErrorCategory.ArgumentValueConversion, inner); + string message = attribute?.GetErrorMessage(argument, value) + ?? argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, EnumType, value, true); + + return new(message, argument.ArgumentName, CommandLineArgumentErrorCategory.ArgumentValueConversion, inner); } } diff --git a/src/Ookii.CommandLine/Convert-SyncMethod.ps1 b/src/Ookii.CommandLine/Convert-SyncMethod.ps1 index 064b005e..2ae0ecfc 100644 --- a/src/Ookii.CommandLine/Convert-SyncMethod.ps1 +++ b/src/Ookii.CommandLine/Convert-SyncMethod.ps1 @@ -23,13 +23,17 @@ $files = Get-Item $Path foreach ($file in $files) { $outputPath = Join-Path $OutputDir ($file.Name.Replace("Async", "Sync")) - Get-Content $file | ForEach-Object { - # Regex replace generic Task before the other replacements. - $result = ($_ -creplace 'async Task<(.*?)>','partial $1') - foreach ($item in $replacements) { - $result = $result.Replace($item[0], $item[1]) - } - $result + &{ + "// " + Get-Content $file | ForEach-Object { + # Regex replace generic Task before the other replacements. + $result = ($_ -creplace 'async Task<(.*?)>','partial $1') + foreach ($item in $replacements) { + $result = $result.Replace($item[0], $item[1]) + } + + $result + } } | Set-Content $outputPath } diff --git a/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs index 8bb0ede7..41afebab 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs @@ -14,7 +14,7 @@ public partial class LineWrappingTextWriter { private partial class LineBuffer { - public async Task FlushToAsync(TextWriter writer, int indent, bool insertNewLine, CancellationToken cancellationToken) + public async Task FlushToAsync(TextWriter writer, int? indent, bool insertNewLine, CancellationToken cancellationToken) { // Don't use IsContentEmpty because we also want to write if there's only VT sequences. if (_segments.Count != 0) @@ -28,7 +28,7 @@ public async Task WriteLineToAsync(TextWriter writer, int indent, CancellationTo await WriteToAsync(writer, indent, true, cancellationToken); } - private async Task WriteToAsync(TextWriter writer, int indent, bool insertNewLine, CancellationToken cancellationToken) + private async Task WriteToAsync(TextWriter writer, int? indent, bool insertNewLine, CancellationToken cancellationToken) { // Don't use IsContentEmpty because we also want to write if there's only VT sequences. if (_segments.Count != 0) @@ -46,7 +46,11 @@ private async Task WriteToAsync(TextWriter writer, int indent, bool insertNewLin private async Task WriteSegmentsAsync(TextWriter writer, IEnumerable segments, CancellationToken cancellationToken) { - await WriteIndentAsync(writer, Indentation); + if (Indentation is int indentation) + { + await WriteIndentAsync(writer, indentation); + } + foreach (var segment in segments) { switch (segment.Type) @@ -109,7 +113,7 @@ private async Task BreakLineAsync(TextWriter writer, ReadO } int offset = 0; - int contentOffset = Indentation; + int contentOffset = Indentation ?? 0; foreach (var segment in _segments) { offset += segment.Length; @@ -168,7 +172,7 @@ private async Task FlushCoreAsync(bool insertNewLine, CancellationToken cancella ThrowIfWriteInProgress(); if (_lineBuffer != null) { - await _lineBuffer.FlushToAsync(_baseWriter, insertNewLine ? _indent : 0, insertNewLine, cancellationToken); + await _lineBuffer.FlushToAsync(_baseWriter, insertNewLine ? _indent : null, insertNewLine, cancellationToken); } await _baseWriter.FlushAsync(); @@ -180,12 +184,12 @@ private async Task ResetIndentCoreAsync(CancellationToken cancellationToken) { if (!_lineBuffer.IsContentEmpty) { - await _lineBuffer.FlushToAsync(_baseWriter, 0, true, cancellationToken); + await _lineBuffer.FlushToAsync(_baseWriter, null, true, cancellationToken); } else { // Leave non-content segments in the buffer. - _lineBuffer.ClearCurrentLine(0, false); + _lineBuffer.ClearCurrentLine(null, false); } } else @@ -254,7 +258,7 @@ await buffer.SplitAsync(true, async (type, span) => private async Task WriteLineBreakDirectAsync(CancellationToken cancellationToken) { await WriteBlankLineAsync(_baseWriter, cancellationToken); - _noWrappingState.IndentNextWrite = _noWrappingState.CurrentLineLength != 0; + _noWrappingState.IndentNextWrite = IndentAfterEmptyLine || _noWrappingState.CurrentLineLength != 0; _noWrappingState.CurrentLineLength = 0; } diff --git a/src/Ookii.CommandLine/LineWrappingTextWriter.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.cs index d7a27f24..7ea6fce0 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.cs @@ -106,12 +106,14 @@ private ref struct BreakLineResult private partial class LineBuffer { + private readonly LineWrappingTextWriter _writer; private readonly RingBuffer _buffer; private readonly List _segments = new(); private bool _hasOverflow; - public LineBuffer(int capacity) + public LineBuffer(LineWrappingTextWriter writer, int capacity) { + _writer = writer; _buffer = new(capacity); } @@ -121,9 +123,13 @@ public LineBuffer(int capacity) public bool IsEmpty => _segments.Count == 0; - public int Indentation { get; set; } + private int? Indentation { get; set; } - public int LineLength => ContentLength + Indentation; + public int LineLength => ContentLength + (Indentation ?? 0); + + public Segment? LastSegment => _segments.Count > 0 ? _segments[_segments.Count - 1] : null; + + public bool HasPartialFormatting => LastSegment is Segment last && last.Type >= StringSegmentType.PartialFormattingUnknown; public void Append(ReadOnlySpan span, StringSegmentType type) { @@ -166,17 +172,17 @@ public void Append(ReadOnlySpan span, StringSegmentType type) ContentLength += contentLength; } - public Segment? LastSegment => _segments.Count > 0 ? _segments[_segments.Count - 1] : null; - - public bool HasPartialFormatting => LastSegment is Segment last && last.Type >= StringSegmentType.PartialFormattingUnknown; - - public partial void FlushTo(TextWriter writer, int indent, bool insertNewLine); + public partial void FlushTo(TextWriter writer, int? indent, bool insertNewLine); public partial void WriteLineTo(TextWriter writer, int indent); public void Peek(TextWriter writer) { - WriteIndent(writer, Indentation); + if (Indentation is int indentation) + { + WriteIndent(writer, indentation); + } + int offset = 0; foreach (var segment in _segments) { @@ -216,7 +222,7 @@ public ReadOnlyMemory FindPartialFormattingEnd(ReadOnlyMemory newSeg return newSegment.Slice(FindPartialFormattingEndCore(newSegment.Span)); } - private partial void WriteTo(TextWriter writer, int indent, bool insertNewLine); + private partial void WriteTo(TextWriter writer, int? indent, bool insertNewLine); private int FindPartialFormattingEndCore(ReadOnlySpan newSegment) { @@ -253,24 +259,34 @@ private int FindPartialFormattingEndCore(ReadOnlySpan newSegment) private partial BreakLineResult BreakLine(TextWriter writer, ReadOnlySpan newSegment, int maxLength, int indent, BreakLineMode mode); - public void ClearCurrentLine(int indent, bool clearSegments = true) + public void ClearCurrentLine(int? indent, bool clearSegments = true) { if (clearSegments) { _segments.Clear(); } - if (!IsContentEmpty) + if (_writer.IndentAfterEmptyLine || !IsContentEmpty) { Indentation = indent; } else { - Indentation = 0; + Indentation = null; } ContentLength = 0; } + + public void UpdateIndent(int indent) + { + // We can apply the new indent immediately if the current line has indentation and is + // blank. + if (IsContentEmpty && Indentation != null) + { + Indentation = indent; + } + } } struct NoWrappingState @@ -337,7 +353,7 @@ public LineWrappingTextWriter(TextWriter baseWriter, int maximumLineLength, bool if (_maximumLineLength > 0) { // Add some slack for formatting characters. - _lineBuffer = new(countFormatting ? _maximumLineLength : _maximumLineLength * 2); + _lineBuffer = new(this, countFormatting ? _maximumLineLength : _maximumLineLength * 2); } } @@ -348,10 +364,7 @@ public LineWrappingTextWriter(TextWriter baseWriter, int maximumLineLength, bool /// /// The that this is writing to. /// - public TextWriter BaseWriter - { - get { return _baseWriter; } - } + public TextWriter BaseWriter => _baseWriter; /// /// Gets the character encoding in which the output is written. @@ -384,10 +397,7 @@ public override string NewLine /// /// The maximum length of a line, or zero if the line length is not limited. /// - public int MaximumLineLength - { - get { return _maximumLineLength; } - } + public int MaximumLineLength => _maximumLineLength; /// /// Gets or sets the amount of characters to indent all but the first line. @@ -397,18 +407,26 @@ public int MaximumLineLength /// /// /// - /// Whenever a line break is encountered (either because of wrapping or because a line break was written to the - /// ), the next line is indented by the number of characters specified - /// by this property, unless the previous line was blank. + /// Whenever a line break is encountered (either because of wrapping or because a line break + /// was written to the ), the next line is indented by the + /// number of characters specified by this property, unless the property is and the previous line + /// was blank. + /// + /// + /// Changes to this property will not /// /// /// The output position can be reset to the start of the line after a line break by calling /// the method. /// /// + /// + /// The property was set to a value less than zero or greater than the maximum line length. + /// public int Indent { - get { return _indent; } + get => _indent; set { if (value < 0 || (_maximumLineLength > 0 && value >= _maximumLineLength)) @@ -417,9 +435,28 @@ public int Indent } _indent = value; + _lineBuffer?.UpdateIndent(value); } } + /// + /// Gets or sets a value which indicates whether a line after an empty line should have + /// indentation. + /// + /// + /// if a line after an empty line should be indented; otherwise, + /// . The default value is . + /// + /// + /// + /// By default, the class will start lines that follow + /// an empty line at the beginning of the line, regardless of the value of the + /// property. Set this property to to apply + /// indentation even to lines following an empty line. + /// + /// + public bool IndentAfterEmptyLine { get; set; } + /// /// Gets or sets a value which indicates how to wrap lines at the maximum line length. /// @@ -456,8 +493,8 @@ public WrappingMode Wrapping { // Flush the buffer but not the base writer, and make sure indent is reset // even if the buffer was empty (for consistency). - _lineBuffer.FlushTo(_baseWriter, 0, false); - _lineBuffer.ClearCurrentLine(0); + _lineBuffer.FlushTo(_baseWriter, null, false); + _lineBuffer.ClearCurrentLine(null); // Ensure no state is carried over from the last time this was changed. _noWrappingState = default; @@ -476,10 +513,7 @@ public WrappingMode Wrapping /// A that writes to , /// the standard output stream. /// - public static LineWrappingTextWriter ForConsoleOut() - { - return new LineWrappingTextWriter(Console.Out, GetLineLengthForConsole(), false); - } + public static LineWrappingTextWriter ForConsoleOut() => new(Console.Out, GetLineLengthForConsole(), false); /// /// Gets a that writes to the standard error stream, @@ -489,10 +523,7 @@ public static LineWrappingTextWriter ForConsoleOut() /// A that writes to , /// the standard error stream. /// - public static LineWrappingTextWriter ForConsoleError() - { - return new LineWrappingTextWriter(Console.Error, GetLineLengthForConsole(), false); - } + public static LineWrappingTextWriter ForConsoleError() => new(Console.Error, GetLineLengthForConsole(), false); /// /// Gets a that writes to a . @@ -511,10 +542,9 @@ public static LineWrappingTextWriter ForConsoleError() /// To retrieve the resulting string, call the method. The result /// will include any unflushed text without flushing that text to the . /// - public static LineWrappingTextWriter ForStringWriter(int maximumLineLength = 0, IFormatProvider? formatProvider = null, bool countFormatting = false) - { - return new LineWrappingTextWriter(new StringWriter(formatProvider), maximumLineLength, true, countFormatting); - } + public static LineWrappingTextWriter ForStringWriter(int maximumLineLength = 0, IFormatProvider? formatProvider = null, + bool countFormatting = false) + => new(new StringWriter(formatProvider), maximumLineLength, true, countFormatting); /// public override void Write(char value) diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Usage.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Usage.cs new file mode 100644 index 00000000..d4e5b323 --- /dev/null +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Usage.cs @@ -0,0 +1,76 @@ +using Ookii.CommandLine.Properties; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine; + +partial class LocalizedStringProvider +{ + /// + /// Gets the default prefix for the usage syntax, used by the class. + /// + /// The string. + public virtual string UsageSyntaxPrefix() => Resources.DefaultUsagePrefix; + + /// + /// Gets the default suffix for the usage syntax when creating command list usage help, used by + /// the class. + /// + /// The string. + public virtual string CommandUsageSuffix() => Resources.DefaultCommandUsageSuffix; + + /// + /// Gets the default suffix for the usage syntax to indicate more arguments are available if the + /// syntax is abbreviated, used by the class. + /// + /// The string. + public virtual string UsageAbbreviatedRemainingArguments() => Resources.DefaultAbbreviatedRemainingArguments; + + /// + /// Gets the default header to print above the list of available commands, used by the + /// class. + /// + /// The string. + public virtual string UsageAvailableCommandsHeader() => Resources.DefaultAvailableCommandsHeader; + + /// + /// Gets the text to use to display a default value in the usage help, used by the + /// class. + /// + /// The argument's default value. + /// + /// An object that provides culture-specific format information for the default value. This + /// will be the value of the property, to ensure the + /// format used matches the format used when parsing arguments. + /// + /// The string. + public virtual string UsageDefaultValue(object defaultValue, IFormatProvider formatProvider) + => string.Format(formatProvider, Resources.DefaultDefaultValueFormat, defaultValue); + + /// + /// Gets a message telling the user how to get more detailed help, used by the + /// class. + /// + /// + /// The application's executable name, optionally including the command name. + /// + /// The name of the help argument, including prefix. + /// The string. + public virtual string UsageMoreInfoMessage(string executableName, string helpArgumentName) + => Format(Resources.MoreInfoOnErrorFormat, executableName, helpArgumentName); + + /// + /// Gets an instruction on how to get help on a command, used by the + /// class. + /// + /// The application and command name. + /// The argument name prefix for the help argument. + /// The help argument name. + /// The string. + public virtual string UsageCommandHelpInstruction(string name, string argumentNamePrefix, string argumentName) + => Format(Resources.CommandHelpInstructionFormat, name, argumentNamePrefix, argumentName); + +} diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs index 51285244..2d1f5176 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs @@ -251,11 +251,19 @@ public virtual string ValidateCountFailed(string argumentName, ValidateCountAttr { if (attribute.Maximum == int.MaxValue) { - return Format(Resources.ValidateCountMinFormat, argumentName, attribute.Minimum); + var format = attribute.Minimum == 1 + ? Resources.ValidateCountMinSingularFormat + : Resources.ValidateCountMinPluralFormat; + + return Format(format, argumentName, attribute.Minimum); } else if (attribute.Minimum <= 0) { - return Format(Resources.ValidateCountMaxFormat, argumentName, attribute.Maximum); + var format = attribute.Maximum == 1 + ? Resources.ValidateCountMaxSingularFormat + : Resources.ValidateCountMaxPluralFormat; + + return Format(format, argumentName, attribute.Maximum); } else { diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.cs b/src/Ookii.CommandLine/LocalizedStringProvider.cs index b2ccec0f..bef1dc15 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.cs @@ -1,5 +1,6 @@ using Ookii.CommandLine.Commands; using Ookii.CommandLine.Properties; +using System; using System.Globalization; using System.Reflection; diff --git a/src/Ookii.CommandLine/NativeMethods.cs b/src/Ookii.CommandLine/NativeMethods.cs deleted file mode 100644 index 50b02b76..00000000 --- a/src/Ookii.CommandLine/NativeMethods.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Ookii.CommandLine.Terminal; -using System; -using System.Runtime.InteropServices; - -namespace Ookii.CommandLine; - -static partial class NativeMethods -{ - static readonly IntPtr INVALID_HANDLE_VALUE = new(-1); - - public static (bool, ConsoleModes?) EnableVirtualTerminalSequences(StandardStream stream, bool enable) - { - if (stream == StandardStream.Input) - { - throw new ArgumentException(Properties.Resources.InvalidStandardStream, nameof(stream)); - } - - var handle = GetStandardHandle(stream); - if (handle == INVALID_HANDLE_VALUE) - { - return (false, null); - } - - if (!GetConsoleMode(handle, out ConsoleModes mode)) - { - return (false, null); - } - - var oldMode = mode; - if (enable) - { - mode |= ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; - } - else - { - mode &= ~ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; - } - - if (oldMode == mode) - { - return (true, null); - } - - if (!SetConsoleMode(handle, mode)) - { - return (false, null); - } - - return (true, oldMode); - } - - public static IntPtr GetStandardHandle(StandardStream stream) - { - var stdHandle = stream switch - { - StandardStream.Output => StandardHandle.STD_OUTPUT_HANDLE, - StandardStream.Input => StandardHandle.STD_INPUT_HANDLE, - StandardStream.Error => StandardHandle.STD_ERROR_HANDLE, - _ => throw new ArgumentException(Properties.Resources.InvalidStandardStream, nameof(stream)), - }; - - return GetStdHandle(stdHandle); - } - -#if NET7_0_OR_GREATER - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool SetConsoleMode(IntPtr hConsoleHandle, ConsoleModes dwMode); - - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool GetConsoleMode(IntPtr hConsoleHandle, out ConsoleModes lpMode); - - [LibraryImport("kernel32.dll", SetLastError = true)] - private static partial IntPtr GetStdHandle(StandardHandle nStdHandle); -#else - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool SetConsoleMode(IntPtr hConsoleHandle, ConsoleModes dwMode); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool GetConsoleMode(IntPtr hConsoleHandle, out ConsoleModes lpMode); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern IntPtr GetStdHandle(StandardHandle nStdHandle); -#endif - - [Flags] - public enum ConsoleModes : uint - { - ENABLE_PROCESSED_INPUT = 0x0001, - ENABLE_LINE_INPUT = 0x0002, - ENABLE_ECHO_INPUT = 0x0004, - ENABLE_WINDOW_INPUT = 0x0008, - ENABLE_MOUSE_INPUT = 0x0010, - ENABLE_INSERT_MODE = 0x0020, - ENABLE_QUICK_EDIT_MODE = 0x0040, - ENABLE_EXTENDED_FLAGS = 0x0080, - ENABLE_AUTO_POSITION = 0x0100, - -#pragma warning disable CA1069 // Enums values should not be duplicated - ENABLE_PROCESSED_OUTPUT = 0x0001, - ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002, - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004, - DISABLE_NEWLINE_AUTO_RETURN = 0x0008, - ENABLE_LVB_GRID_WORLDWIDE = 0x0010 -#pragma warning restore CA1069 // Enums values should not be duplicated - } - - private enum StandardHandle - { - STD_OUTPUT_HANDLE = -11, - STD_INPUT_HANDLE = -10, - STD_ERROR_HANDLE = -12, - } -} diff --git a/src/Ookii.CommandLine/NativeMethods.txt b/src/Ookii.CommandLine/NativeMethods.txt new file mode 100644 index 00000000..501bcc2c --- /dev/null +++ b/src/Ookii.CommandLine/NativeMethods.txt @@ -0,0 +1,5 @@ +CONSOLE_MODE +STD_HANDLE +GetConsoleMode +GetStdHandle +SetConsoleMode \ No newline at end of file diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index d876bf46..e89c8fcd 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -1,4 +1,4 @@ - + net8.0;net7.0;net6.0;netstandard2.0;netstandard2.1 @@ -58,9 +58,8 @@ library for .Net applications. - + all - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 8d2615f7..20c60b7d 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -222,9 +222,11 @@ public virtual bool IsPosix /// /// /// - /// This property is only used if the if the parsing mode is set to , - /// either using the property or the - /// attribute + /// This property is only used if the property or the + /// is + /// , or if the + /// or property is not + /// . /// /// /// Use the to specify the prefixes for short argument @@ -730,6 +732,47 @@ public LocalizedStringProvider StringProvider /// public NameTransform ValueDescriptionTransformOrDefault => ValueDescriptionTransform ?? NameTransform.None; + /// + /// Gets or sets the behavior when an argument is encountered that consists of only the long + /// argument prefix ("--" by default) by itself, not followed by a name. + /// + /// + /// One of the values of the enumeration, or + /// to use the value from the + /// attribute, or if that is not present, . + /// The default value is . + /// + /// + /// + /// Use this property to allow the use of the long argument prefix by itself to either treat + /// all remaining values as positional argument values, even when they start with an argument + /// prefix, or to cancel parsing with so + /// the remaining values can be inspected using the + /// property. This follows + /// typical POSIX argument parsing conventions. + /// + /// + /// The value of the property is used to identify this + /// special argument, even if the parsing mode is not + /// . + /// + /// + /// If not , this property overrides the + /// property. + /// + /// + public PrefixTerminationMode? PrefixTermination { get; set; } + + /// + /// Gets the behavior when an argument is encountered that consists of only the long argument + /// prefix ("--" by default) by itself, not followed by a name. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public PrefixTerminationMode PrefixTerminationOrDefault => PrefixTermination ?? PrefixTerminationMode.None; + /// /// Gets or sets a value that indicates whether the class /// will use reflection even if the command line arguments type has the @@ -807,17 +850,31 @@ public void Merge(ParseOptionsAttribute attribute) AutoVersionArgument ??= attribute.AutoVersionArgument; AutoPrefixAliases ??= attribute.AutoPrefixAliases; ValueDescriptionTransform ??= attribute.ValueDescriptionTransform; + PrefixTermination ??= attribute.PrefixTermination; } - internal VirtualTerminalSupport? EnableErrorColor() + internal VirtualTerminalSupport EnableErrorColor() { - if (Error == null && UseErrorColor == null) + // If colors are forced on or off; don't change terminal mode but return the explicit + // support value. + if (UseErrorColor is bool useErrorColor) + { + return new VirtualTerminalSupport(useErrorColor); + } + + // Enable for stderr if no custom error writer. + if (Error == null) + { + return VirtualTerminal.EnableColor(StandardStream.Error); + } + + // Try to enable it for the std stream associated with the custom writer. + if (Error.GetStandardStream() is StandardStream stream) { - var support = VirtualTerminal.EnableColor(StandardStream.Error); - UseErrorColor = support.IsSupported; - return support; + return new VirtualTerminalSupport(stream); } - return null; + // No std stream, no automatic color. + return new VirtualTerminalSupport(false); } } diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index c28e0420..bcf7ab27 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -155,9 +155,11 @@ public virtual bool IsPosix /// /// /// - /// This property is only used if the property is - /// , or if the parsing mode is set to - /// elsewhere. + /// This property is only used if the or + /// property is + /// , or if the + /// or property is not + /// . /// /// /// Use the to specify the prefixes for short argument @@ -408,6 +410,35 @@ public virtual bool IsPosix /// public NameTransform ValueDescriptionTransform { get; set; } + /// + /// Gets or sets the behavior when an argument is encountered that consists of only the long + /// argument prefix ("--" by default) by itself, not followed by a name. + /// + /// + /// One of the values of the enumeration. The default value + /// is . + /// + /// + /// + /// Use this property to allow the use of the long argument prefix by itself to either treat + /// all remaining values as positional argument values, even when they start with an argument + /// prefix, or to cancel parsing with so + /// the remaining values can be inspected using the + /// property. This follows + /// typical POSIX argument parsing conventions. + /// + /// + /// The value of the property is used to identify this + /// special argument, even if the parsing mode is not + /// . + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + public PrefixTerminationMode PrefixTermination { get; set; } + internal StringComparison GetStringComparison() { if (CaseSensitive) diff --git a/src/Ookii.CommandLine/PrefixTerminationMode.cs b/src/Ookii.CommandLine/PrefixTerminationMode.cs new file mode 100644 index 00000000..b4029cfc --- /dev/null +++ b/src/Ookii.CommandLine/PrefixTerminationMode.cs @@ -0,0 +1,28 @@ +namespace Ookii.CommandLine; + +/// +/// Indicates the effect of an argument that is just the long argument prefix ("--" by default) +/// by itself, not followed by a name. +/// +/// +/// +public enum PrefixTerminationMode +{ + /// + /// There is no special behavior for the argument. + /// + None, + /// + /// The argument terminates the use of named arguments. Any following arguments are interpreted + /// as values for positional arguments, even if they begin with a long or short argument name + /// prefix. + /// + PositionalOnly, + /// + /// The argument cancels parsing, returning an instance of the arguments type and making the + /// values after this argument available in the + /// property. This is identical to how an argument with + /// behaves. + /// + CancelWithSuccess +} diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 5f491e31..e07aa888 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -411,6 +411,15 @@ internal static string InvalidStandardStream { } } + /// + /// Looks up a localized string similar to Invalid StandardStream value.. + /// + internal static string InvalidStandardStreamError { + get { + return ResourceManager.GetString("InvalidStandardStreamError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid value for the StringComparison enumeration.. /// @@ -646,7 +655,7 @@ internal static string UsageWriterPropertyNotAvailable { } /// - /// Looks up a localized string similar to The argument '{0}' must have between {1} and {2} items.. + /// Looks up a localized string similar to The argument '{0}' must have between {1} and {2} values.. /// internal static string ValidateCountBothFormat { get { @@ -655,20 +664,38 @@ internal static string ValidateCountBothFormat { } /// - /// Looks up a localized string similar to The argument '{0}' must have at most {1} item(s).. + /// Looks up a localized string similar to The argument '{0}' must have at most {1} values.. + /// + internal static string ValidateCountMaxPluralFormat { + get { + return ResourceManager.GetString("ValidateCountMaxPluralFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' must have at most {1} value.. + /// + internal static string ValidateCountMaxSingularFormat { + get { + return ResourceManager.GetString("ValidateCountMaxSingularFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' must have at least {1} values.. /// - internal static string ValidateCountMaxFormat { + internal static string ValidateCountMinPluralFormat { get { - return ResourceManager.GetString("ValidateCountMaxFormat", resourceCulture); + return ResourceManager.GetString("ValidateCountMinPluralFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The argument '{0}' must have at least {1} item(s).. + /// Looks up a localized string similar to The argument '{0}' must have at least {1} value.. /// - internal static string ValidateCountMinFormat { + internal static string ValidateCountMinSingularFormat { get { - return ResourceManager.GetString("ValidateCountMinFormat", resourceCulture); + return ResourceManager.GetString("ValidateCountMinSingularFormat", resourceCulture); } } diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index ac855b2d..d39a7870 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -256,13 +256,13 @@ The 'minimum' and 'maximum' parameters cannot both be null. - The argument '{0}' must have between {1} and {2} items. + The argument '{0}' must have between {1} and {2} values. - - The argument '{0}' must have at most {1} item(s). + + The argument '{0}' must have at most {1} values. - - The argument '{0}' must have at least {1} item(s). + + The argument '{0}' must have at least {1} values. The argument '{0}' must not be empty. @@ -408,4 +408,13 @@ The type '{0}' is not an enumeration type. + + Invalid StandardStream value. + + + The argument '{0}' must have at most {1} value. + + + The argument '{0}' must have at least {1} value. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index c40bd54d..e96258cf 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -1,6 +1,7 @@ using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Ookii.CommandLine.Support; @@ -26,7 +27,11 @@ public abstract class ArgumentProvider /// if there is none. /// /// The class validators for the arguments type. - protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, IEnumerable? validators) + protected ArgumentProvider( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] +#endif + Type argumentsType, ParseOptionsAttribute? options, IEnumerable? validators) { ArgumentsType = argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)); OptionsAttribute = options; @@ -47,6 +52,9 @@ protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, I /// /// The of the class that will hold the argument values. /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] +#endif public Type ArgumentsType { get; } /// @@ -65,6 +73,15 @@ protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, I /// public abstract string Description { get; } + /// + /// Gets footer text that is used when generating usage information. + /// + /// + /// The footer text. + /// + // N.B. This is virtual, not abstract, for binary compatibility with v4.0. + public virtual string UsageFooter => string.Empty; + /// /// Gets the that was applied to the arguments type. /// diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index d866abce..de41395f 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Reflection; namespace Ookii.CommandLine.Support; @@ -22,6 +23,7 @@ public class GeneratedArgument : CommandLineArgument private readonly Func? _callMethod; private readonly string _defaultValueDescription; private readonly string? _defaultKeyDescription; + private MemberInfo? _member; private GeneratedArgument(ArgumentInfo info, Action? setProperty, Func? getProperty, Func? callMethod, string defaultValueDescription, string? defaultKeyDescription) : base(info) @@ -137,6 +139,10 @@ public static GeneratedArgument Create(CommandLineParser parser, return new GeneratedArgument(info, setProperty, getProperty, callMethod, defaultValueDescription, defaultKeyDescription); } + /// + public override MemberInfo? Member => _member ??= (MemberInfo?)Parser.ArgumentsType.GetProperty(MemberName) + ?? Parser.ArgumentsType.GetMethod(MemberName, BindingFlags.Public | BindingFlags.Static); + /// protected override bool CanSetProperty => _setProperty != null; diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index e2d4b446..09be869c 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Ookii.CommandLine.Support; @@ -36,7 +37,11 @@ public abstract class GeneratedArgumentProvider : ArgumentProvider /// The for the arguments type, or if /// there is none. /// - protected GeneratedArgumentProvider(Type argumentsType, + protected GeneratedArgumentProvider( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] +#endif + Type argumentsType, ParseOptionsAttribute? options = null, IEnumerable? validators = null, ApplicationFriendlyNameAttribute? friendlyName = null, diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index f01b610b..bdfa2195 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -37,6 +37,8 @@ private ReflectionArgument(ArgumentInfo info, PropertyInfo? property, MethodArgu _method = method; } + public override MemberInfo? Member => (MemberInfo?)_property ?? _method?.Method; + protected override bool CanSetProperty => _property?.GetSetMethod() != null; protected override void SetProperty(object target, object? value) diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 8aac2a0e..e17547da 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -35,6 +35,8 @@ public override string ApplicationFriendlyName public override string Description => ArgumentsType.GetCustomAttribute()?.Description ?? string.Empty; + public override string UsageFooter => ArgumentsType.GetCustomAttribute()?.Footer ?? string.Empty; + public override bool IsCommand => CommandInfo.IsCommand(ArgumentsType); public override object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues) diff --git a/src/Ookii.CommandLine/Terminal/StandardStreamExtensions.cs b/src/Ookii.CommandLine/Terminal/StandardStreamExtensions.cs new file mode 100644 index 00000000..9d76ae2f --- /dev/null +++ b/src/Ookii.CommandLine/Terminal/StandardStreamExtensions.cs @@ -0,0 +1,158 @@ +using System; +using System.IO; + +namespace Ookii.CommandLine.Terminal; + +/// +/// Provides extension methods for the enumeration. +/// +/// +public static class StandardStreamExtensions +{ + /// + /// Gets the for either + /// or . + /// + /// A value. + /// + /// The value of either or + /// . + /// + /// + /// was a value other than + /// or . + /// + /// + /// + /// The returned instance should not be disposed by the + /// caller. + /// + /// + public static TextWriter GetWriter(this StandardStream stream) + { + return stream switch + { + StandardStream.Output => Console.Out, + StandardStream.Error => Console.Error, + _ => throw new ArgumentException(Properties.Resources.InvalidStandardStreamError, nameof(stream)), + }; + } + + /// + /// Creates a instance for a . + /// + /// A value. + /// + /// The return value of either , + /// , or + /// . + /// + /// + /// was not a valid value. + /// + /// + /// + /// The returned instance should be disposed by the caller. + /// + /// + public static Stream OpenStream(this StandardStream stream) + { + return stream switch + { + StandardStream.Output => Console.OpenStandardOutput(), + StandardStream.Error => Console.OpenStandardError(), + StandardStream.Input => Console.OpenStandardInput(), + _ => throw new ArgumentException(Properties.Resources.InvalidStandardStreamError, nameof(stream)), + }; + } + + /// + /// Gets the associated with a instance, + /// if that instance is for either the standard output or error stream. + /// + /// The . + /// + /// The that is writing to, or + /// if it's not writing to either the standard output or standard error + /// stream. + /// + /// + /// is . + /// + /// + /// + /// If is an instance of the + /// class, the value of the + /// property will be checked. + /// + /// + public static StandardStream? GetStandardStream(this TextWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (writer is LineWrappingTextWriter lwtw) + { + writer = lwtw.BaseWriter; + } + + if (writer == Console.Out) + { + return StandardStream.Output; + } + else if (writer == Console.Error) + { + return StandardStream.Error; + } + + return null; + } + + /// + /// Gets a value that indicates whether the specified standard stream is redirected. + /// + /// The value. + /// + /// if the standard stream indicated by is + /// redirected; otherwise, . + /// + public static bool IsRedirected(this StandardStream stream) + { + return stream switch + { + StandardStream.Output => Console.IsOutputRedirected, + StandardStream.Error => Console.IsErrorRedirected, + StandardStream.Input => Console.IsInputRedirected, + _ => false, + }; + } + + /// + /// Gets the associated with a instance, + /// if that instance is for the standard input stream. + /// + /// The . + /// + /// The that is reading from, or + /// if it's not reading from the standard input stream. + /// + /// + /// is . + /// + public static StandardStream? GetStandardStream(this TextReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (reader == Console.In) + { + return StandardStream.Input; + } + + return null; + } +} diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs index d4e52022..0adb0710 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs @@ -1,5 +1,10 @@ -using System; +using Microsoft.Win32.SafeHandles; +using System; +using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.System.Console; namespace Ookii.CommandLine.Terminal; @@ -26,8 +31,9 @@ public static class VirtualTerminal /// The to enable VT sequences for. /// /// An instance of the class that will disable - /// virtual terminal support when disposed or destructed. Use the - /// property to check if virtual terminal sequences are supported. + /// virtual terminal support when disposed or finalized. Use the + /// property to check if + /// virtual terminal sequences are supported. /// /// /// @@ -37,19 +43,17 @@ public static class VirtualTerminal /// environment variable is defined. /// /// - /// For , this method does nothing and always returns - /// . + /// If you also want to check for a NO_COLOR environment variable, use the + /// method instead. + /// + /// + /// For , this method does nothing and + /// always returns . /// /// public static VirtualTerminalSupport EnableVirtualTerminalSequences(StandardStream stream) { - bool supported = stream switch - { - StandardStream.Output => !Console.IsOutputRedirected, - StandardStream.Error => !Console.IsErrorRedirected, - _ => false, - }; - + bool supported = stream != StandardStream.Input && !stream.IsRedirected(); if (!supported) { return new VirtualTerminalSupport(false); @@ -63,19 +67,16 @@ public static VirtualTerminalSupport EnableVirtualTerminalSequences(StandardStre if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var (enabled, previousMode) = NativeMethods.EnableVirtualTerminalSequences(stream, true); - if (!enabled) + var result = SetVirtualTerminalSequences(stream, true); + if (result.NeedRestore) { - return new VirtualTerminalSupport(false); + Debug.Assert(result.Supported); + return new VirtualTerminalSupport(stream); } - if (previousMode is NativeMethods.ConsoleModes mode) - { - return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), mode); - } - - // Support was already enabled externally, so don't change the console mode on dispose. - return new VirtualTerminalSupport(true); + // VT sequences are either not supported, or were already enabled so we don't need to + // disable them. + return new VirtualTerminalSupport(result.Supported); } // Support is assumed on non-Windows platforms if TERM is set. @@ -109,6 +110,59 @@ public static VirtualTerminalSupport EnableColor(StandardStream stream) return EnableVirtualTerminalSequences(stream); } + /// + /// Writes a line to the standard output stream which, if virtual terminal sequences are + /// supported, will use the specified formatting. + /// + /// The text to write. + /// The formatting that should be applied to the text. + /// + /// The VT sequence that should be used to undo the formatting, or to + /// use . + /// + /// + /// + /// This method takes care of checking whether VT sequences are supported by using the + /// method, and on Windows, will reset the console mode afterwards + /// if needed. + /// + /// + /// The and parameters will be ignored + /// if the standard output stream does not support VT sequences. In that case, the value of + /// will be written without formatting. + /// + /// + public static void WriteLineFormatted(string text, TextFormat textFormat, TextFormat? reset = null) + => WriteLineFormatted(StandardStream.Output, Console.Out, text, textFormat, reset ?? TextFormat.Default); + + /// + /// Writes a line to the standard error stream which, if virtual terminal sequences are + /// supported, will use the specified formatting. + /// + /// The text to write. + /// + /// The formatting that should be applied to the text, or to use + /// . + /// + /// + /// The VT sequence that should be used to undo the formatting, or to + /// use . + /// + /// + /// + /// This method takes care of checking whether VT sequences are supported by using the + /// method, and on Windows, will reset the console mode afterwards + /// if needed. + /// + /// + /// The and parameters will be ignored + /// if the standard error stream does not support VT sequences. In that case, the value of + /// will be written without formatting. + /// + /// + public static void WriteLineErrorFormatted(string text, TextFormat? textFormat = null, TextFormat? reset = null) + => WriteLineFormatted(StandardStream.Error, Console.Error, text, textFormat ?? TextFormat.ForegroundRed, reset ?? TextFormat.Default); + // Returns the index of the character after the end of the sequence. internal static int FindSequenceEnd(ReadOnlySpan value, ref StringSegmentType type) { @@ -134,6 +188,23 @@ internal static int FindSequenceEnd(ReadOnlySpan value, ref StringSegmentT }; } + private static void WriteLineFormatted(StandardStream stream, TextWriter writer, string text, TextFormat textFormat, TextFormat reset) + { + using var support = EnableColor(stream); + if (support.IsSupported) + { + writer.Write(textFormat); + } + + writer.Write(text); + if (support.IsSupported) + { + writer.Write(reset); + } + + writer.WriteLine(); + } + private static int FindCsiEnd(ReadOnlySpan value, ref StringSegmentType type) { int result = FindCsiEndPartial(value.Slice(1), ref type); @@ -195,4 +266,61 @@ private static int FindOscEndPartial(ReadOnlySpan value, ref StringSegment type = hasEscape ? StringSegmentType.PartialFormattingOscWithEscape : StringSegmentType.PartialFormattingOsc; return -1; } + + internal static (bool Supported, bool NeedRestore) SetVirtualTerminalSequences(StandardStream stream, bool enable) + { + if (stream == StandardStream.Input) + { + throw new ArgumentException(Properties.Resources.InvalidStandardStream, nameof(stream)); + } + + // Dispose should not close the handle here, but use it anyway. + using var handle = GetStandardHandle(stream); + if (handle.IsInvalid) + { + return (false, false); + } + + if (!PInvoke.GetConsoleMode(handle, out var mode)) + { + return (false, false); + } + + var oldMode = mode; + if (enable) + { + mode |= CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + else + { + mode &= ~CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + + if (oldMode == mode) + { + return (true, false); + } + + if (!PInvoke.SetConsoleMode(handle, mode)) + { + return (false, false); + } + + return (true, true); + } + + private static SafeFileHandle GetStandardHandle(StandardStream stream) + { + var stdHandle = stream switch + { + StandardStream.Output => STD_HANDLE.STD_OUTPUT_HANDLE, + StandardStream.Input => STD_HANDLE.STD_INPUT_HANDLE, + StandardStream.Error => STD_HANDLE.STD_ERROR_HANDLE, + _ => throw new ArgumentException(Properties.Resources.InvalidStandardStream, nameof(stream)), + }; + + // Generated function uses ownsHandle: false so the standard handle is not closed, as + // expected. + return PInvoke.GetStdHandle_SafeHandle(stdHandle); + } } diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs index c4387ce1..17e1301f 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs @@ -12,21 +12,18 @@ namespace Ookii.CommandLine.Terminal; /// public sealed class VirtualTerminalSupport : IDisposable { - private readonly bool _supported; - private IntPtr _handle; - private readonly NativeMethods.ConsoleModes _previousMode; + private StandardStream? _restoreStream; internal VirtualTerminalSupport(bool supported) { - _supported = supported; + IsSupported = supported; GC.SuppressFinalize(this); } - internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previousMode) + internal VirtualTerminalSupport(StandardStream restoreStream) { - _supported = true; - _handle = handle; - _previousMode = previousMode; + IsSupported = true; + _restoreStream = restoreStream; } /// @@ -52,7 +49,7 @@ internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previo /// if virtual terminal sequences are supported; otherwise, /// . /// - public bool IsSupported => _supported; + public bool IsSupported { get; } /// /// Cleans up resources for the class. @@ -73,10 +70,10 @@ public void Dispose() private void ResetConsoleMode() { - if (_handle != IntPtr.Zero) + if (_restoreStream is StandardStream stream) { - NativeMethods.SetConsoleMode(_handle, _previousMode); - _handle = IntPtr.Zero; + VirtualTerminal.SetVirtualTerminalSequences(stream, false); + _restoreStream = null; } } } diff --git a/src/Ookii.CommandLine/UnknownArgumentEventArgs.cs b/src/Ookii.CommandLine/UnknownArgumentEventArgs.cs new file mode 100644 index 00000000..7e481713 --- /dev/null +++ b/src/Ookii.CommandLine/UnknownArgumentEventArgs.cs @@ -0,0 +1,138 @@ +using System; + +namespace Ookii.CommandLine; + +/// +/// Provides data for the event. +/// +/// +public class UnknownArgumentEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The argument token that contains the unknown argument. + /// The argument name. + /// The argument value. + /// + /// Indicates whether the argument is part of a combined short switch argument. + /// + /// + /// is . + /// + public UnknownArgumentEventArgs(string token, ReadOnlyMemory name, ReadOnlyMemory value, bool isCombinedSwithToken) + { + Token = token ?? throw new ArgumentNullException(nameof(token)); + Name = name; + Value = value; + IsCombinedSwitchToken = isCombinedSwithToken; + } + + /// + /// Gets the token for the unknown argument. + /// + /// + /// The raw token value. + /// + /// + /// + /// For an unknown named argument, the token includes the prefix, and the value if one was + /// present using a non-whitespace separator. For example, "-Name:Value" or "--name". + /// + /// + /// If the unknown argument was part of a combined short switch argument when using + /// , the property + /// will contain all the switch names, while the property only contains the + /// name of the unknown switch. For example, the token could be "-xyz" while the name is + /// "y". + /// + /// + /// For an unknown positional argument value, the property is equal to + /// the property. + /// + /// + public string Token { get; } + + /// + /// Gets the name of the unknown argument. + /// + /// + /// The argument name, or an empty span if this was an unknown positional argument value. + /// + /// + /// + /// If the unknown argument was part of a combined short switch argument when using + /// , the property + /// will contain all the switch names, while the property only contains the + /// name of the unknown switch. For example, the token could be "-xyz" while the name is + /// "y". + /// + /// + public ReadOnlyMemory Name { get; } + + /// + /// Gets the value of the unknown argument. + /// + /// + /// The argument value, or an empty span if this was a named argument that did not contain a + /// value using a non-whitespace separator. + /// + public ReadOnlyMemory Value { get; } + + /// + /// Gets a value that indicates whether this argument is one among a token containing several + /// combined short name switches. + /// + /// + /// if the unknown argument is part of a combined switch argument when + /// using ; otherwise, + /// . + /// + /// + /// + /// If the unknown argument was part of a combined short switch argument when using + /// , the property + /// will contain all the switch names, while the property only contains the + /// name of the unknown switch. For example, the token could be "-xyz" while the name is + /// "y". + /// + /// + public bool IsCombinedSwitchToken { get; } + + /// + /// Gets or sets a value that indicates whether the unknown argument will be ignored. + /// + /// + /// to ignore the unknown argument and continue parsing with the + /// remaining arguments; for the default behavior where parsing fails. + /// The default value is + /// + /// + /// + /// This property is not used if the property is set to a value + /// other than . + /// + /// + public bool Ignore { get; set; } + + /// + /// Gets or sets a value that indicates whether parsing should be canceled when the event + /// handler returns. + /// + /// + /// One of the values of the enumeration. The default value is + /// . + /// + /// + /// + /// If the event handler sets this property to a value other than , + /// command line processing will stop immediately, returning either or + /// an instance of the arguments class according to the value. + /// + /// + /// If you want usage help to be displayed after canceling, set the + /// property to . + /// + /// + public CancelMode CancelParsing { get; set; } +} diff --git a/src/Ookii.CommandLine/UsageFooterAttribute.cs b/src/Ookii.CommandLine/UsageFooterAttribute.cs new file mode 100644 index 00000000..3d29418f --- /dev/null +++ b/src/Ookii.CommandLine/UsageFooterAttribute.cs @@ -0,0 +1,61 @@ +using System; +using System.ComponentModel; + +namespace Ookii.CommandLine; + +/// +/// Gets or sets a footer that will be added to the usage help for an arguments class. +/// +/// +/// +/// The attribute provides text that's written at the top of +/// the usage help. The attribute does the same thing, but for +/// text that's written at the bottom of the usage help. +/// +/// +/// The footer will only be used when the full usage help is shown, using +/// . +/// +/// +/// You can derive from this attribute to use an alternative source for the footer, such as a +/// resource table that can be localized. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public class UsageFooterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The footer text. + /// + /// is . + /// + public UsageFooterAttribute(string footer) + { + FooterValue = footer ?? throw new ArgumentNullException(nameof(footer)); + } + + /// + /// Gets the footer text. + /// + /// + /// The footer text. + /// + public virtual string Footer => FooterValue; + + /// + /// Gets the footer text stored in this attribute. + /// + /// + /// The footer text. + /// + /// + /// + /// The base class implementation of the property returns the value of + /// this property. + /// + /// + protected string FooterValue { get; } +} diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index 02e4e6cb..10002b05 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -96,8 +96,10 @@ protected enum Operation private const char OptionalStart = '['; private const char OptionalEnd = ']'; + private readonly LineWrappingTextWriter? _customWriter; private LineWrappingTextWriter? _writer; - private bool? _useColor; + private readonly bool? _useColor; + private bool _autoColor; private CommandLineParser? _parser; private CommandManager? _commandManager; private string? _executableName; @@ -127,7 +129,7 @@ protected enum Operation /// public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) { - _writer = writer; + _customWriter = writer; _useColor = useColor; } @@ -481,13 +483,36 @@ public bool IncludeExecutableExtension /// public string? CommandName { get; set; } + /// + /// Gets or sets a value which indicates whether a line after an empty line should have + /// indentation. + /// + /// + /// if a line after an empty line should be indented; otherwise, + /// . The default value is . + /// + /// + /// + /// By default, the class will start lines that follow an empty line + /// at the beginning of the line, regardless of the value of the , + /// , or + /// property. Set this property to to apply indentation even to lines + /// following an empty line. + /// + /// + /// This can be useful if you have argument descriptions that contain blank lines when + /// argument descriptions are indented, such as in the default format. + /// + /// + public bool IndentAfterEmptyLine { get; set; } + /// /// Gets or sets a value that indicates whether the usage help should use color. /// /// /// to enable color output; otherwise, . /// - protected bool UseColor => _useColor ?? false; + protected bool UseColor => _useColor ?? _autoColor; /// /// Gets or sets the color applied by the base implementation of the @@ -649,6 +674,9 @@ protected LineWrappingTextWriter Writer /// /// Gets the that usage is being written for. /// + /// + /// An instance of the class. + /// /// /// A operation is not in progress. /// @@ -656,14 +684,31 @@ protected CommandLineParser Parser => _parser ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); /// - /// Gets the that usage is being written for. + /// Gets the that usage is being written for. /// + /// + /// An instance of the class. + /// /// /// A operation is not in progress. /// protected CommandManager CommandManager => _commandManager ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + /// + /// Gets the implementation used to get strings for + /// error messages and usage help. + /// + /// + /// An instance of a class inheriting from the class. + /// + /// + /// A operation is not in progress. + /// + protected LocalizedStringProvider StringProvider + => _parser?.StringProvider ?? _commandManager?.Options.StringProvider + ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + /// /// Indicates what operation is currently in progress. /// @@ -882,6 +927,7 @@ protected virtual void WriteParserUsageCore(UsageHelpRequest request) WriteArgumentDescriptions(); Writer.Indent = 0; + WriteParserUsageFooter(); } else { @@ -995,7 +1041,7 @@ protected virtual void WriteParserUsageSyntax() protected virtual void WriteUsageSyntaxPrefix() { WriteColor(UsagePrefixColor); - Write(Resources.DefaultUsagePrefix); + Write(StringProvider.UsageSyntaxPrefix()); ResetColor(); Write(' '); Write(ExecutableName); @@ -1023,7 +1069,7 @@ protected virtual void WriteUsageSyntaxSuffix() { if (OperationInProgress == Operation.CommandListUsage) { - WriteLine(Resources.DefaultCommandUsageSuffix); + WriteLine(StringProvider.CommandUsageSuffix()); } } @@ -1195,7 +1241,7 @@ protected virtual void WriteValueDescription(string valueDescription) /// /// protected virtual void WriteAbbreviatedRemainingArguments() - => Write(Resources.DefaultAbbreviatedRemainingArguments); + => Write(StringProvider.UsageAbbreviatedRemainingArguments()); /// /// Writes a suffix that indicates an argument is a multi-value argument. @@ -1437,7 +1483,15 @@ protected virtual void WriteArgumentDescriptionBody(CommandLineArgument argument if (IncludeDefaultValueInDescription && argument.IncludeDefaultInUsageHelp && argument.DefaultValue != null) { - WriteDefaultValue(argument.DefaultValue); + var defaultValue = argument.DefaultValue; + if (argument.DefaultValueFormat != null) + { + // Use the parser's culture so the format matches the format the user should use + // for values. + defaultValue = string.Format(argument.Parser.Culture, argument.DefaultValueFormat, defaultValue); + } + + WriteDefaultValue(defaultValue); } WriteLine(); @@ -1634,9 +1688,22 @@ protected virtual void WriteArgumentValidators(CommandLineArgument argument) /// and the property /// is not . /// + /// + /// If the + /// property for the argument is not , then the base implementation of + /// the method will use the formatted string, + /// rather than the original default value, for the + /// parameter. + /// + /// + /// The default implementation formats the argument using the culture specified by the + /// property, rather than the + /// culture used by the output , so that the displayed format will match + /// the format the user should use for argument values. + /// /// protected virtual void WriteDefaultValue(object defaultValue) - => Write(Resources.DefaultDefaultValueFormat, defaultValue); + => Write(StringProvider.UsageDefaultValue(defaultValue, Parser.Culture)); /// /// Writes a message telling to user how to get more detailed help. @@ -1666,7 +1733,30 @@ protected virtual void WriteMoreInfoMessage() name += " " + CommandName; } - WriteLine(Resources.MoreInfoOnErrorFormat, name, arg.ArgumentNameWithPrefix); + WriteLine(StringProvider.UsageMoreInfoMessage(name, arg.ArgumentNameWithPrefix)); + } + } + + /// + /// Writes a footer under the usage help. + /// + /// + /// + /// This method is called by the base implementation of + /// only if the requested help is . + /// + /// + /// The base implementation writes the value of the + /// property, if it is not an empty string. This value can be set using the + /// attribute. + /// + /// + protected virtual void WriteParserUsageFooter() + { + if (!string.IsNullOrEmpty(Parser.UsageFooter)) + { + WriteLine(Parser.UsageFooter); + WriteLine(); } } @@ -1720,8 +1810,9 @@ protected virtual IEnumerable GetArgumentsInDescriptionOrde /// /// /// The base implementation writes the application description, followed by the list - /// of commands, followed by a message indicating how to get help on a command. Which - /// elements are included exactly can be influenced by the properties of this class. + /// of commands, followed by a footer, which may include a message indicating how to get help + /// on a command. Which elements are included exactly can be influenced by the properties of + /// this class. /// /// protected virtual void WriteCommandListUsageCore() @@ -1742,25 +1833,8 @@ protected virtual void WriteCommandListUsageCore() WriteAvailableCommandsHeader(); WriteCommandDescriptions(); - - if (CheckShowCommandHelpInstruction()) - { - var prefix = CommandManager.Options.Mode == ParsingMode.LongShort - ? (CommandManager.Options.LongArgumentNamePrefixOrDefault) - : (CommandManager.Options.ArgumentNamePrefixes?.FirstOrDefault() ?? CommandLineParser.GetDefaultArgumentNamePrefixes()[0]); - - var transform = CommandManager.Options.ArgumentNameTransformOrDefault; - var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); - - Writer.Indent = 0; - var name = ExecutableName; - if (CommandName != null) - { - name += " " + CommandName; - } - - WriteCommandHelpInstruction(name, prefix, argumentName); - } + Writer.Indent = 0; + WriteCommandListUsageFooter(); } /// @@ -1797,7 +1871,7 @@ protected virtual void WriteCommandListUsageSyntax() /// protected virtual void WriteAvailableCommandsHeader() { - WriteLine(Resources.DefaultAvailableCommandsHeader); + WriteLine(StringProvider.UsageAvailableCommandsHeader()); WriteLine(); } @@ -1956,6 +2030,39 @@ protected virtual void WriteCommandAliases(IEnumerable aliases) protected virtual void WriteCommandDescription(string description) => Write(description); + /// + /// Writes a footer underneath the command list usage. + /// + /// + /// + /// The base implementation calls the method if the + /// help instruction is explicitly or automatically enabled. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteCommandListUsageFooter() + { + if (CheckShowCommandHelpInstruction()) + { + var prefix = CommandManager.Options.Mode == ParsingMode.LongShort + ? (CommandManager.Options.LongArgumentNamePrefixOrDefault) + : (CommandManager.Options.ArgumentNamePrefixes?.FirstOrDefault() ?? CommandLineParser.GetDefaultArgumentNamePrefixes()[0]); + + var transform = CommandManager.Options.ArgumentNameTransformOrDefault; + var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); + var name = ExecutableName; + if (CommandName != null) + { + name += " " + CommandName; + } + + WriteCommandHelpInstruction(name, prefix, argumentName); + } + } + /// /// Writes an instruction on how to get help on a command. /// @@ -1968,14 +2075,14 @@ protected virtual void WriteCommandDescription(string description) /// information on a command." /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method if the property is , /// or if it is and all commands meet the requirements. /// /// protected virtual void WriteCommandHelpInstruction(string name, string argumentNamePrefix, string argumentName) { - WriteLine(Resources.CommandHelpInstructionFormat, name, argumentNamePrefix, argumentName); + WriteLine(StringProvider.UsageCommandHelpInstruction(name, argumentNamePrefix, argumentName)); } #endregion @@ -2034,6 +2141,15 @@ protected virtual void WriteSpacing(int count) /// protected virtual void WriteLine() => Writer.WriteLine(); + /// + /// Writes a string to the , followed by a line break. + /// + /// The string to write. + protected void WriteLine(string? value) + { + Write(value); + WriteLine(); + } /// /// Writes a string with virtual terminal sequences only if color is enabled. @@ -2096,26 +2212,21 @@ internal string GetArgumentUsage(CommandLineArgument argument) return writer.BaseWriter.ToString()!; } - private void WriteLine(string? value) - { - Write(value); - WriteLine(); - } - - private void Write(string format, object? arg0) => Write(string.Format(Writer.FormatProvider, format, arg0)); - - private void WriteLine(string format, object? arg0, object? arg1) - => WriteLine(string.Format(Writer.FormatProvider, format, arg0, arg1)); - - private void WriteLine(string format, object? arg0, object? arg1, object? arg2) - => WriteLine(string.Format(Writer.FormatProvider, format, arg0, arg1, arg2)); - private VirtualTerminalSupport? EnableColor() { - if (_useColor == null && _writer == null) + if (_useColor == null) { - var support = VirtualTerminal.EnableColor(StandardStream.Output); - _useColor = support.IsSupported; + VirtualTerminalSupport? support = null; + if (_customWriter == null) + { + support = VirtualTerminal.EnableColor(StandardStream.Output); + } + else if (_customWriter.GetStandardStream() is StandardStream stream) + { + support = VirtualTerminal.EnableColor(stream); + } + + _autoColor = support?.IsSupported ?? false; return support; } @@ -2149,52 +2260,28 @@ private int WriteAliasHelper(string prefix, IEnumerable? aliases, int coun private void WriteUsageInternal(UsageHelpRequest request = UsageHelpRequest.Full) { - bool restoreColor = _useColor == null; - bool restoreWriter = _writer == null; - try - { - using var support = EnableColor(); - using var writer = DisposableWrapper.Create(_writer, LineWrappingTextWriter.ForConsoleOut); - _writer = writer.Inner; - Writer.ResetIndent(); - Writer.Indent = 0; - RunOperation(request); - } - finally - { - if (restoreColor) - { - _useColor = null; - } - - if (restoreWriter) - { - _writer = null; - } - } + using var support = EnableColor(); + using var writer = DisposableWrapper.Create(_customWriter, LineWrappingTextWriter.ForConsoleOut); + _writer = writer.Inner; + Writer.ResetIndent(); + Writer.Indent = 0; + RunOperation(request); } private string GetUsageInternal(int maximumLineLength = 0, UsageHelpRequest request = UsageHelpRequest.Full) { - var originalWriter = _writer; - try - { - using var writer = LineWrappingTextWriter.ForStringWriter(maximumLineLength); - _writer = writer; - RunOperation(request); - writer.Flush(); - return writer.BaseWriter.ToString()!; - } - finally - { - _writer = originalWriter; - } + using var writer = LineWrappingTextWriter.ForStringWriter(maximumLineLength); + _writer = writer; + RunOperation(request); + writer.Flush(); + return writer.BaseWriter.ToString()!; } private void RunOperation(UsageHelpRequest request) { try { + Writer.IndentAfterEmptyLine = IndentAfterEmptyLine; if (_parser == null) { WriteCommandListUsageCore(); @@ -2208,6 +2295,8 @@ private void RunOperation(UsageHelpRequest request) { _parser = null; _commandManager = null; + _writer = null; + _autoColor = false; } } diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index 409a34ca..056c3ce2 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -6,7 +6,8 @@ namespace Ookii.CommandLine.Validation; /// /// Validates whether the value of an enumeration type is one of the defined values for that -/// type. +/// type, and provides additional conversion options for enumeration types converted using the +/// class. /// /// /// @@ -25,11 +26,18 @@ namespace Ookii.CommandLine.Validation; /// enumeration, by using the method. /// /// +/// This validator can also alter the behavior of the class, through +/// the , , and +/// properties. These properties are only effective if +/// the default class is used, or a custom converter that also checks +/// them. +/// +/// /// In addition, this validator provides usage help listing all the possible values. If the /// enumeration has a lot of values, you may wish to turn this off by setting the -/// property to -/// . Similarly, you can avoid listing all the values in the error -/// message by setting the property to +/// +/// property to . Similarly, you can avoid listing all the values in the +/// error message by setting the property to /// . /// /// @@ -39,6 +47,98 @@ namespace Ookii.CommandLine.Validation; /// public class ValidateEnumValueAttribute : ArgumentValidationWithHelpAttribute { + /// + /// Gets or sets a value that indicates whether values that do not match one of the + /// enumeration's defined values are allowed. + /// + /// + /// if values that are not defined by the enumeration are allowed; + /// otherwise, . The default value is . + /// + /// + /// + /// Non-defined values can be provided using the underlying numeric type of the enumeration. + /// If this property is , this validator will not check whether a value + /// provided in such a way actually is actually one of the enumeration's defined values. + /// + /// + /// Setting this to essentially makes this validator do nothing. It + /// is useful if you want to use it solely to list defined values in the usage help, or if + /// you want to use one of the other properties that affect the + /// class without also checking for defined values. + /// + /// + public bool AllowNonDefinedValues { get; set; } + + /// + /// Gets or sets a value that indicates whether the possible values of the enumeration + /// should be included in the error message if validation fails. + /// + /// + /// to include the values; otherwise, . The + /// default value is . + /// + /// + /// + /// This property is used when validation fails, and is also checked by the + /// class when conversion fails due to an invalid string value. + /// + /// + public bool IncludeValuesInErrorMessage { get; set; } = true; + + /// + /// Gets or sets a value that indicates whether enumeration value conversion is case sensitive. + /// + /// + /// if conversion is case sensitive; otherwise, . + /// The default value is . + /// + /// + /// + /// This property is not used by the class itself, + /// but by the class. Therefore, this property may not work if + /// a custom argument converter is used, unless that custom converter also checks this + /// property. + /// + /// + public bool CaseSensitive { get; set; } + + /// + /// Gets or sets a value that indicates whether the value provided by the user can use commas + /// to provide multiple values that will be combined with bitwise-or. + /// + /// + /// if comma-separated values are allowed; otherwise, + /// . The default value is . + /// + /// + /// + /// This property is not used by the class itself, + /// but by the class. Therefore, this property may not work if + /// a custom argument converter is used, unless that custom converter also checks this + /// property. + /// + /// + public bool AllowCommaSeparatedValues { get; set; } = true; + + /// + /// Gets or sets a value that indicates whether the value provided by the user can the + /// underlying numeric type of the enumeration. + /// + /// + /// if numeric values are allowed; otherwise, to + /// allow only value names. The default value is . + /// + /// + /// + /// This property is not used by the class itself, + /// but by the class. Therefore, this property may not work if + /// a custom argument converter is used, unless that custom converter also checks this + /// property. + /// + /// + public bool AllowNumericValues { get; set; } = true; + /// /// Determines if the argument's value is defined. /// @@ -52,26 +152,9 @@ public override bool IsValid(CommandLineArgument argument, object? value) Properties.Resources.ArgumentNotEnumFormat, argument.ArgumentName)); } - return value == null || argument.ElementType.IsEnumDefined(value); + return AllowNonDefinedValues || value == null || argument.ElementType.IsEnumDefined(value); } - /// - /// Gets or sets a value that indicates whether the possible values of the enumeration - /// should be included in the error message if validation fails. - /// - /// - /// to include the values; otherwise, . - /// - /// - /// - /// This property is only used if the validation fails, which only the case for - /// undefined numerical values. Other strings that don't match the name of one of the - /// defined constants use the error message from the converter, which in the case of - /// the always shows the possible values. - /// - /// - public bool IncludeValuesInErrorMessage { get; set; } - /// /// /// @@ -94,4 +177,40 @@ protected override string GetUsageHelpCore(CommandLineArgument argument) public override string GetErrorMessage(CommandLineArgument argument, object? value) => argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, argument.ElementType, value, IncludeValuesInErrorMessage); + + internal bool ValidateBeforeConversion(CommandLineArgument argument, ReadOnlySpan value) + { + if (!AllowCommaSeparatedValues && value.IndexOf(',') >= 0) + { + return false; + } + + if (!AllowNumericValues) + { + var current = value; + while (current.Length > 0) + { + if (char.IsDigit(current[0]) || current.StartsWith(argument.Parser.Culture.NumberFormat.NegativeSign.AsSpan())) + { + return false; + } + + if (!AllowCommaSeparatedValues) + { + // There can't be any commas, checked above. + break; + } + + var index = current.IndexOf(','); + if (index < 0) + { + break; + } + + current = current.Slice(index + 1); + } + } + + return true; + } } diff --git a/src/Samples/ArgumentDependencies/README.md b/src/Samples/ArgumentDependencies/README.md index ee7205ee..0d5ef495 100644 --- a/src/Samples/ArgumentDependencies/README.md +++ b/src/Samples/ArgumentDependencies/README.md @@ -47,9 +47,9 @@ validators like [`ValidateRangeAttribute`][]), and all the included validators c case-by-case basis with the [`IncludeInUsageHelp`][IncludeInUsageHelp_0] property on each validator attribute. -[`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm -[`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm -[`RequiresAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm -[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm +[`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm +[`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm +[`RequiresAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm +[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm +[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm +[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm diff --git a/src/Samples/CustomUsage/README.md b/src/Samples/CustomUsage/README.md index c4d4bf97..2e57e277 100644 --- a/src/Samples/CustomUsage/README.md +++ b/src/Samples/CustomUsage/README.md @@ -50,5 +50,5 @@ If you compare this with the usage output of the [parser sample](../Parser), whi output format, you can see just how much you can change by simply overriding some methods on the [`UsageWriter`][] class. -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm diff --git a/src/Samples/LongShort/README.md b/src/Samples/LongShort/README.md index 6b337975..b700bc0d 100644 --- a/src/Samples/LongShort/README.md +++ b/src/Samples/LongShort/README.md @@ -75,4 +75,4 @@ argument names. Long/short mode allows you to combine switches with short names, so running `LongShort -vp` sets both `--verbose` and `--process` to true. -[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm +[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm diff --git a/src/Samples/NestedCommands/BaseCommand.cs b/src/Samples/NestedCommands/BaseCommand.cs index ca529ec5..512d2018 100644 --- a/src/Samples/NestedCommands/BaseCommand.cs +++ b/src/Samples/NestedCommands/BaseCommand.cs @@ -24,35 +24,16 @@ public override async Task RunAsync() } catch (IOException ex) { - WriteErrorMessage(ex.Message); + VirtualTerminal.WriteLineErrorFormatted(ex.Message); return (int)ExitCode.IOError; } catch (UnauthorizedAccessException ex) { - WriteErrorMessage(ex.Message); + VirtualTerminal.WriteLineErrorFormatted(ex.Message); return (int)ExitCode.IOError; } } // Derived classes will implement this instead of the normal RunAsync. protected abstract Task RunAsync(Database db); - - // Helper method to print error messages. - private static void WriteErrorMessage(string message) - { - using var support = VirtualTerminal.EnableColor(StandardStream.Error); - using var writer = LineWrappingTextWriter.ForConsoleError(); - - // Add some color if we can. - if (support.IsSupported) - { - writer.Write(TextFormat.ForegroundRed); - } - - writer.WriteLine(message); - if (support.IsSupported) - { - writer.Write(TextFormat.Default); - } - } } diff --git a/src/Samples/NestedCommands/README.md b/src/Samples/NestedCommands/README.md index 877d138b..8b9f8231 100644 --- a/src/Samples/NestedCommands/README.md +++ b/src/Samples/NestedCommands/README.md @@ -106,9 +106,9 @@ Usage: NestedCommands student add [-FirstName] [-LastName] [[- The usage syntax shows both command names before the arguments. -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`CommandOptions.ParentCommand`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_ParentCommand.htm -[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm -[`ParentCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommand.htm -[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`CommandOptions.ParentCommand`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_Commands_CommandOptions_ParentCommand.htm +[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm +[`ParentCommand`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ParentCommand.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm diff --git a/src/Samples/Parser/ProgramArguments.cs b/src/Samples/Parser/ProgramArguments.cs index 8b1d33ae..836e66b2 100644 --- a/src/Samples/Parser/ProgramArguments.cs +++ b/src/Samples/Parser/ProgramArguments.cs @@ -140,6 +140,9 @@ partial class ProgramArguments // though DayOfWeek has no member with the value 9. The ValidateEnumValueAttribute makes sure // that only defined enum values are allowed. As a bonus, it also adds all the possible values // to the usage help. + // + // If conversion fails, the error message also lists all the possible values. If you don't want + // that, you can use [ValidateEnumValue(IncludeValuesInErrorMessage = false)] [CommandLineArgument] [Description("This is an argument using an enumeration type.")] [ValidateEnumValue] diff --git a/src/Samples/Parser/README.md b/src/Samples/Parser/README.md index 758efe29..d5da6682 100644 --- a/src/Samples/Parser/README.md +++ b/src/Samples/Parser/README.md @@ -89,6 +89,6 @@ The `-Version` argument shows the value of the [`ApplicationFriendlyNameAttribut assembly title or name, if there isn't one), the assembly's informational version, and the assembly's copyright text. -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.1/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm +[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm diff --git a/src/Samples/Subcommand/Program.cs b/src/Samples/Subcommand/Program.cs index 09a07ee0..ca14134a 100644 --- a/src/Samples/Subcommand/Program.cs +++ b/src/Samples/Subcommand/Program.cs @@ -1,65 +1,37 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; -using Ookii.CommandLine.Terminal; +using SubcommandSample; // For an application using subcommands, set the friendly name used for the automatic version // command by using this attribute on the assembly rather than an arguments type. +// You can also use the property in the .csproj file. [assembly: ApplicationFriendlyName("Ookii.CommandLine Subcommand Sample")] -namespace SubcommandSample; - -static class Program +// You can use the CommandOptions class to customize the parsing behavior and usage help +// output. CommandOptions inherits from ParseOptions so it supports all the same options. +var options = new CommandOptions() { - // No need to use the Main(string[] args) overload (though you can if you want), because - // CommandManager can take the arguments directly from Environment.GetCommandLineArgs(). - static async Task Main() + // Set options so the command names are determined by the class name, transformed to + // dash-case and with the "Command" suffix stripped. + CommandNameTransform = NameTransform.DashCase, + UsageWriter = new UsageWriter() { - // You can use the CommandOptions class to customize the parsing behavior and usage help - // output. CommandOptions inherits from ParseOptions so it supports all the same options. - var options = new CommandOptions() - { - // Set options so the command names are determined by the class name, transformed to - // dash-case and with the "Command" suffix stripped. - CommandNameTransform = NameTransform.DashCase, - UsageWriter = new UsageWriter() - { - // Show the application description before the command list. - IncludeApplicationDescriptionBeforeCommandList = true, - }, - }; - - // Create a CommandManager for the commands in the current assembly. We use the manager we - // defined to use source generation, which allows trimming even when using commands. - // - // In addition to our commands, it will also have an automatic "version" command (this can - // be disabled with the options). - var manager = new GeneratedManager(options); - - // Run the command indicated in the first argument to this application, and use the return - // value of its Run method as the application exit code. If the command could not be - // created, we return an error code. - // - // We use the async version because our commands use the IAsyncCommand interface. Note that - // you can use this method even if not all of your commands use IAsyncCommand. - return await manager.RunCommandAsync() ?? (int)ExitCode.CreateCommandFailure; - } - - // Utility method used by the commands to write exception messages to the console. - public static void WriteErrorMessage(string message) - { - using var support = VirtualTerminal.EnableColor(StandardStream.Error); - using var writer = LineWrappingTextWriter.ForConsoleError(); - - // Add some color if we can. - if (support.IsSupported) - { - writer.Write(TextFormat.ForegroundRed); - } - - writer.WriteLine(message); - if (support.IsSupported) - { - writer.Write(TextFormat.Default); - } - } -} + // Show the application description before the command list. + IncludeApplicationDescriptionBeforeCommandList = true, + }, +}; + +// Create a CommandManager for the commands in the current assembly. We use the manager we +// defined to use source generation, which allows trimming even when using commands. +// +// In addition to our commands, it will also have an automatic "version" command (this can +// be disabled with the options). +var manager = new GeneratedManager(options); + +// Run the command indicated in the first argument to this application, and use the return +// value of its Run method as the application exit code. If the command could not be +// created, we return an error code. +// +// We use the async version because our commands use the IAsyncCommand interface. Note that +// you can use this method even if not all of your commands use IAsyncCommand. +return await manager.RunCommandAsync() ?? (int)ExitCode.CreateCommandFailure; diff --git a/src/Samples/Subcommand/README.md b/src/Samples/Subcommand/README.md index a690d3e8..aca44c35 100644 --- a/src/Samples/Subcommand/README.md +++ b/src/Samples/Subcommand/README.md @@ -83,8 +83,8 @@ Copyright (c) Sven Groot (Ookii.org) This is sample code, so you can use it freely. ``` -[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm [Encoding_1]: https://learn.microsoft.com/dotnet/api/system.text.encoding diff --git a/src/Samples/Subcommand/ReadCommand.cs b/src/Samples/Subcommand/ReadCommand.cs index c7f3e864..af63dc25 100644 --- a/src/Samples/Subcommand/ReadCommand.cs +++ b/src/Samples/Subcommand/ReadCommand.cs @@ -1,6 +1,7 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Terminal; using System.ComponentModel; using System.Text; @@ -58,12 +59,12 @@ public override async Task RunAsync() } catch (IOException ex) { - Program.WriteErrorMessage(ex.Message); + VirtualTerminal.WriteLineErrorFormatted(ex.Message); return (int)ExitCode.ReadWriteFailure; } catch (UnauthorizedAccessException ex) { - Program.WriteErrorMessage(ex.Message); + VirtualTerminal.WriteLineErrorFormatted(ex.Message); return (int)ExitCode.ReadWriteFailure; } } diff --git a/src/Samples/Subcommand/WriteCommand.cs b/src/Samples/Subcommand/WriteCommand.cs index 714433b0..90e7916a 100644 --- a/src/Samples/Subcommand/WriteCommand.cs +++ b/src/Samples/Subcommand/WriteCommand.cs @@ -1,6 +1,7 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Terminal; using Ookii.CommandLine.Validation; using System.ComponentModel; using System.Text; @@ -66,7 +67,7 @@ public override async Task RunAsync() // The Main method will return the exit status to the operating system. The numbers // are made up for the sample, they don't mean anything. Usually, 0 means success, // and any other value indicates an error. - Program.WriteErrorMessage("File already exists."); + VirtualTerminal.WriteLineErrorFormatted("File already exists."); return (int)ExitCode.FileExists; } @@ -93,12 +94,12 @@ public override async Task RunAsync() } catch (IOException ex) { - Program.WriteErrorMessage(ex.Message); + VirtualTerminal.WriteLineErrorFormatted(ex.Message); return (int)ExitCode.ReadWriteFailure; } catch (UnauthorizedAccessException ex) { - Program.WriteErrorMessage(ex.Message); + VirtualTerminal.WriteLineErrorFormatted(ex.Message); return (int)ExitCode.ReadWriteFailure; } } diff --git a/src/Samples/TopLevelArguments/README.md b/src/Samples/TopLevelArguments/README.md index d50c1bf7..26cf2ca8 100644 --- a/src/Samples/TopLevelArguments/README.md +++ b/src/Samples/TopLevelArguments/README.md @@ -5,7 +5,7 @@ than using a base class with the common arguments, which makes the common argume command (as shown in the [nested commands sample](../NestedCommands)), this sample defines several top-level arguments that are not part of any command. -The commands themselves are based on the regular [subcommand sample](../SubCommand), so see that for +The commands themselves are based on the regular [subcommand sample](../Subcommand), so see that for more detailed descriptions. This sample uses POSIX conventions, for variation, but this isn't required. @@ -83,4 +83,4 @@ Usage: TopLevelArguments [global arguments] write [[--lines] ...] [--he When this option is specified, the file will be overwritten if it already exists. ``` -[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm +[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CancelMode.htm diff --git a/src/Samples/Wpf/README.md b/src/Samples/Wpf/README.md index 057fbe6a..80989db5 100644 --- a/src/Samples/Wpf/README.md +++ b/src/Samples/Wpf/README.md @@ -33,7 +33,7 @@ A similar approach would work for Windows Forms, or any other GUI framework. This application is very basic; it's just a sample, and I don't do a lot of GUI work nowadays. It's just intended to show how the [`UsageWriter`][] can be adapted to work in the context of a GUI app. -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm -[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.1/html/T_Ookii_CommandLine_UsageWriter.htm +[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.1/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.1/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm diff --git a/src/Samples/Wpf/Wpf.csproj b/src/Samples/Wpf/Wpf.csproj index 21591029..98c52290 100644 --- a/src/Samples/Wpf/Wpf.csproj +++ b/src/Samples/Wpf/Wpf.csproj @@ -14,7 +14,7 @@ This is sample code, so you can use it freely. - +