diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3eed6947..b4194bff 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -21,7 +21,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore src - name: Build diff --git a/README.md b/README.md index b04a678a..abc131ac 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Ookii.CommandLine [![NuGet](https://img.shields.io/nuget/v/Ookii.CommandLine)](https://www.nuget.org/packages/Ookii.CommandLine/) -Ookii.CommandLine is a powerful and flexible command line argument parsing library for .Net -applications. +Ookii.CommandLine is a powerful, flexible and highly customizable command line argument parsing +library for .Net applications. - Easily define arguments by creating a class with properties. - Create applications with multiple subcommands. - Generate fully customizable usage help. - Supports PowerShell-like and POSIX-like parsing rules. +- Trim-friendly -Ookii.CommandLine is provided in versions for [.Net Standard 2.0, .Net Standard 2.1, and .Net 6.0 and later](#requirements). +Ookii.CommandLine is [provided in versions](#requirements) for .Net Standard 2.0, .Net Standard 2.1, +.Net 6.0, and .Net 7.0 and later. Ookii.CommandLine can be added to your project using [NuGet](https://nuget.org/packages/Ookii.CommandLine). [Code snippets](docs/CodeSnippets.md) for Visual Studio are available on the @@ -20,35 +22,36 @@ A [C++ version](https://github.com/SvenGroot/Ookii.CommandLine.Cpp) is also avai Ookii.CommandLine is a library that lets you parse the command line arguments for your application into a set of strongly-typed, named values. You can easily define the accepted arguments, and then -parse the command line supplied to your application for those arguments. In addition, you can -generate usage help that can be displayed to the user. +parse the supplied arguments for those values. In addition, you can generate usage help that can be +displayed to the user. Ookii.CommandLine can be used with any kind of .Net application, whether console or GUI. Some -functions, such as creating usage help, are primarily designed for console applications, but even -those can be easily adapted for use with other styles of applications. +functionality, such as creating usage help, is primarily designed for console applications, but +even those can be easily adapted for use with other styles of applications. Two styles of [command line parsing rules](docs/Arguments.md) are supported: the default mode uses rules similar to those used by PowerShell, and the alternative [long/short mode](docs/Arguments.md#longshort-mode) uses a style influenced by POSIX conventions, where arguments have separate long and short names with different prefixes. Many aspects of the parsing rules are configurable. -To determine which arguments are accepted, you create a class, with constructor parameters and -properties that define the arguments. Attributes are used to specify names, create required or -positional arguments, and to specify descriptions for use in the generated usage help. +To determine which arguments are accepted, you create a class, with properties and methods that +define the arguments. Attributes are used to specify names, create required or positional arguments, +and to specify descriptions for use in the generated usage help. For example, the following class defines four arguments: a required positional argument, an optional positional argument, a named-only argument, and a switch argument (sometimes also called a flag): ```csharp -class MyArguments +[GeneratedParser] +partial class MyArguments { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("A required positional argument.")] - public string? Required { get; set; } + public required string Required { get; set; } - [CommandLineArgument(Position = 1)] + [CommandLineArgument(IsPositional = true)] [Description("An optional positional argument.")] - public int Optional { get; set; } + public int Optional { get; set; } = 42; [CommandLineArgument] [Description("An argument that can only be supplied by name.")] @@ -62,13 +65,18 @@ class MyArguments Each argument has a different type that determines the kinds of values it can accept. +> If you are using an older version of .Net where the `required` keyword is not available, you can +> use `[CommandLineArgument(IsRequired = true)]` to create a required argument instead. + To parse these arguments, all you have to do is add the following line to your `Main` method: ```csharp -var arguments = CommandLineParser.Parse(); +var arguments = MyArguments.Parse(); ``` -This code will take the arguments from `Environment.GetCommandLineArgs()` (you can also manually +The `Parse()` method is added to the class through [source generation](docs/SourceGeneration.md). + +This method will take the arguments from `Environment.GetCommandLineArgs()` (you can also manually pass a `string[]` array if you want), will handle and print errors to the console, and will print usage help if needed. It returns an instance of `MyArguments` if successful, and `null` if not. @@ -83,7 +91,7 @@ Usage: MyApplication [-Required] [[-Optional] ] [-Help] [-Named A required positional argument. -Optional - An optional positional argument. + An optional positional argument with a default value. Default value: 42. -Help [] (-?, -h) Displays this help message. @@ -98,10 +106,9 @@ Usage: MyApplication [-Required] [[-Optional] ] [-Help] [-Named Displays version information. ``` -The [usage help](docs/UsageHelp.md) includes the descriptions given for the arguments. - -See the [documentation for the samples](src/Samples) for more examples of usage help generated by -Ookii.CommandLine. The usage help format can also be [fully customized](src/Samples/CustomUsage). +The [usage help](docs/UsageHelp.md) includes the descriptions given for the arguments, as well as +things like default values and aliases. The usage help format can also be +[fully customized](src/Samples/CustomUsage). The application also has two arguments that weren't in the class, `-Help` and `-Version`, which are automatically added by default. @@ -126,22 +133,28 @@ It can be used with applications supporting one of the following: - .Net Standard 2.0 - .Net Standard 2.1 -- .Net 6.0 and later +- .Net 6.0 +- .Net 7.0 and later As of version 3.0, .Net Framework 2.0 is no longer supported. You can still target .Net Framework 4.6.1 and later using the .Net Standard 2.0 assembly. If you need to support an older version of .Net, please continue to use [version 2.4](https://github.com/SvenGroot/ookii.commandline/releases/tag/v2.4). -The .Net Standard 2.1 and .Net 6.0 version utilize `ReadOnlySpan` for improved performance of -the [`LineWrappingTextWriter`](docs/Utilities.md) class. +The .Net Standard 2.1 and .Net 6.0 and 7.0 versions utilize the framework `ReadOnlySpan` and +`ReadOnlyMemory` types without a dependency on the System.Memory package. + +The .Net 6.0 version has additional support for [nullable reference types](docs/Arguments.md#arguments-with-non-nullable-types), +and is annotated to allow [trimming](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options) +when [source generation](docs/SourceGeneration.md) is used. -The .Net 6.0 version has additional support for [nullable reference types](docs/Arguments.md#arguments-with-non-nullable-types). +The .Net 7.0 version has additional support for `required` properties, and can utilize +`ISpanParsable` and `IParsable` for argument value conversions. ## Building and testing To build Ookii.CommandLine, make sure you have the following installed: -- [Microsoft .Net 6.0 SDK](https://dotnet.microsoft.com/download) +- [Microsoft .Net 7.0 SDK](https://dotnet.microsoft.com/download) or later - [Microsoft PowerShell 6 or later](https://github.com/PowerShell/PowerShell) PowerShell is used to generate some source files during the build. Besides installing it normally, @@ -151,8 +164,8 @@ To build the library, tests and samples, simply use the `dotnet build` command i directory. You can run the unit tests using `dotnet test`. The tests should pass on all platforms (Windows and Linux have been tested). -The tests are built and run for both .Net 6.0 and .Net Framework 4.8. Running the .Net Framework -tests on a non-Windows platform may require the use of [Mono](https://www.mono-project.com/). +The tests are built and run for .Net 7.0, .Net 6.0, and .Net Framework 4.8. Running the .Net +Framework tests on a non-Windows platform may require the use of [Mono](https://www.mono-project.com/). Ookii.CommandLine uses a strongly-typed resources file, which will not update correctly unless the `Resources.resx` file is edited with [Microsoft Visual Studio](https://visualstudio.microsoft.com/). @@ -168,22 +181,27 @@ arguments; the only way was a third-party library, or do it yourself. Nowadays, System.CommandLine offers an official Microsoft solution for command line parsing. Why, then, should you use Ookii.CommandLine? -Ookii.CommandLine has a very different design. It uses a declarative approach to defining command -line arguments, using properties and attributes, which I personally prefer as it reduces the amount -of code you typically need to write. +Here are some of the most important differences (as of this writing, and to the best of my knowledge): -Additionally, Ookii.CommandLine supports a more PowerShell-like syntax, as well as the POSIX-like -syntax that System.CommandLine uses. +Ookii.CommandLine | System.CommandLine +------------------------------------------------------------------------------|--------------------------------------------------------------------------------------- +Declarative approach to defining arguments with properties and attributes. | Fluent API with a builder pattern to define arguments. +Supports PowerShell-like and POSIX-like parsing rules. | Supports POSIX-like rules with some modifications. +Supports any type with a `Parse()` method or constructor that takes a string. | Supports a limited number of types, and requires custom conversion methods for others. +Supports automatic prefix aliases. | Does not support automatic prefix aliases. +Does not support middleware or dependency injection. | Supports middleware and dependency injection. +Fully released with a stable API between major releases. | Still in preview. -In the end, it comes down to personal preference. You should use whichever one suits your needs and -coding style best. +These are by no means the only differences. Both are highly customizable, and each has its pros and +cons. In the end, it mostly comes down to personal preference. You should use whichever one suits +your needs and coding style best. ## More information Please check out the following to get started: - [Tutorial: getting started with Ookii.CommandLine](docs/Tutorial.md) -- [Migrating from Ookii.CommandLine 2.x](docs/Migrating.md) +- [Migrating from Ookii.CommandLine 2.x / 3.x](docs/Migrating.md) - [Usage documentation](docs/README.md) - [Class library documentation](https://www.ookii.org/Link/CommandLineDoc) - [Sample applications](src/Samples) with detailed explanations and sample output. diff --git a/docs/Arguments.md b/docs/Arguments.md index f4889e79..b3908b8d 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -9,12 +9,11 @@ through the parameter of the `static void Main(string[] args)` method. This prov as an array of strings, which Ookii.CommandLine will parse to extract strongly-typed, named values that you can easily access in your application. -The method used to extract values from the array of string arguments is determined by the command -line argument parsing rules. Ookii.CommandLine supports two sets of parsing rules: the default mode, -which uses parsing rules similar to those used by PowerShell, and [long/short mode](#longshort-mode), -which is more POSIX-like, and lets arguments have a long name and a short name, with different -prefixes. Most of the below information applies to both modes, with the differences described at the -end. +The way the raw string arguments are interpreted is determined by the command line argument parsing +rules. Ookii.CommandLine supports two sets of parsing rules: the default mode, which uses parsing +rules similar to those used by PowerShell, and [long/short mode](#longshort-mode), which is more +POSIX-like, and lets arguments have a long name and a short name, with different prefixes. Most of +the below information applies to both modes, with the differences described when applicable. ## Named arguments @@ -28,61 +27,114 @@ prompt, and typically take the following form: The argument name is preceded by the _argument name prefix_. This prefix is configurable, but defaults to accepting a dash (`-`) and a forward slash (`/`) on Windows, and only a dash (`-`) on -other platforms such as Linux or MacOS. +other platforms such as Linux or MacOS. In long/short mode, this may be the long argument name +prefix, which is `--` by default. Argument names are case insensitive by default, though this can be customized using the -[`ParseOptionsAttribute.CaseSensitive`][] property or the [`ParseOptions.ArgumentNameComparer`][] +[`ParseOptionsAttribute.CaseSensitive`][] property or the [`ParseOptions.ArgumentNameComparison`][] property. -The argument's value follow the name, separated by either white space (as a separate argument token), -or by the argument name/value separator, which is a colon (`:`) by default. The following is -identical to the previous example: +The argument's value follows the name, separated by either white space (as a separate argument +token), or by the argument name/value separator; by default, both a colon (`:`) and an equals sign +(`=`) are accepted. The following three example are identical: ```text +-ArgumentName value -ArgumentName:value +-ArgumentName=value ``` Whether white-space is allowed to separate the name and value is configured using the [`ParseOptionsAttribute.AllowWhiteSpaceValueSeparator`][] or -[`ParseOptions.AllowWhiteSpaceValueSeparator`][] property, and the argument name/value separator can -be customized using the [`ParseOptionsAttribute.NameValueSeparator`][] or -[`ParseOptions.NameValueSeparator`][] property. +[`ParseOptions.AllowWhiteSpaceValueSeparator`][] property, and the argument name/value separator(s) +can be customized using the [`ParseOptionsAttribute.NameValueSeparators`][] or +[`ParseOptions.NameValueSeparators`][] property. + +The name/value separator cannot occur in the argument name; however, it can still be used in +argument values. For example, `-ArgumentName:foo:bar` will give `-ArgumentName` the value `foo:bar`. Not all arguments require values; those that do not are called [_switch arguments_](#switch-arguments) and have a value determined by their presence or absence on the command line. +### Short names + +In long/short mode, an argument can have an additional, one-character short name. This short name +is often the first character of the long name, but it can be any character. Where long names in +long/short mode use the long argument prefix (`--` by default), short names have their own prefix, +which is `-` (and on Windows, `/`) by default. + +For example, if the argument `--argument-name` has the short name `-a`, the following are equivalent: + +```text +--argument-name value +-a value +``` + +### Aliases + An argument can have one or more aliases: alternative names that can also be used to supply the same argument. For example, an argument named `-Verbose` might use the alias `-v` as a shorter to type -alternative. +alternative. In long/short mode, an argument can have both long and short aliases. + +By default, Ookii.CommandLine accepts [any prefix](DefiningArguments.md#automatic-prefix-aliases) +that uniquely identifies a single argument as an alias for that argument, without having to +explicitly define those aliases. + +For example, if you have two arguments named `-File` and `-Folder`, you can refer to the first +argument with `-Fi` and `-Fil` (also case insensitive by default). For the second one, `-Fo`, +`-Fol`, `-Fold` and `-Folde`. However, `-F` is not an automatic prefix alias, because it could refer +to either argument. + +When using long/short mode, automatic prefix aliases apply to arguments' long names. An argument +named `--argument` can automatically be used with the prefix alias `--a` (assuming it is unique), +but the short name `-a` will only exist if it was explicitly created. ## Positional arguments An argument can be _positional_, which means in addition to being supplied by name, it can also be -supplied without the name, using the position of the value. Which argument the value belongs to +supplied without the name, using the ordering of the values. Which argument the value belongs to is determined by its position relative to other positional arguments. If an argument value is encountered without being preceded by a name, it is matched to the -next positional argument without a value. For example, take the following command line arguments: +next positional argument without a value. For example, take an application that has three arguments: +`-Positional1`, `-Positional2` and `-Positional3` are positional, in that order, and `-NamedOnly` is +non-positional. + +Now, consider the following invocation: ```text -value1 –ArgumentName value2 value3 +value1 -NamedOnly value2 value3 ``` -In this case, value1 is not preceded by a name; therefore, it is matched to the first positional -argument. Value2 follows a name, so it is matched to the argument with the name `-ArgumentName`. -Finally, value3 is matched to the second positional argument. +In this case, "value1" is not preceded by a name; therefore, it is matched to the argument +`-Positional1`. The value "value2" follows a name, so it is matched to the argument with the name +`-NamedOnly`. Finally, "value3" is matched to the second positional argument, which is +`-Positional2`. A positional argument can still be supplied by name. If a positional argument is supplied by name, -it cannot also be specified by position; in the previous example, if the argument named -`-ArgumentName` was the second positional argument, then value3 becomes the value for the third -positional argument, because the value for `-ArgumentName` was already specified by name. If -`-ArgumentName` is the first positional argument, this would cause an error (unless duplicate -arguments are allowed in the options), because it already had a value set by `value`. +it cannot also be specified by position. Take the following example: + +```text +value1 -Positional2 value2 value3 +``` + +In this case, "value1" is still matched to `-Positional1`. The value for `-Positional2` is now +given by name, and is "value2". The value "value3" is for the next positional argument, but since +`-Positional2` already has a value, it will be assigned to `-Positional3` instead. + +The following example would cause an error: + +```text +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. ## Required arguments -A command line argument that is required must be supplied on all invocations of the application. If a -required argument is not supplied, this is considered an error and parsing will fail. +A command line argument that is required must be supplied on all invocations of the application. If +a required argument is not supplied, this is considered an error and parsing will fail. Any argument can be made required. Usually, it is recommended for any required argument to also be a positional argument, but this is not mandatory. @@ -107,12 +159,27 @@ A switch argument’s value can be specified explicitly, as in the following exa -Switch:false ``` -You must use the name/value separator (a colon by default) to specify an explicit value for a switch -argument; you cannot use white space. If the command line contains `-Switch false`, then `false` is -the value of the next positional argument, not the value for `-Switch`. +You must use the name/value separator (a colon or equals sign by default) to specify an explicit +value for a switch argument; you cannot use white space. If the command line contains `-Switch false`, +then `false` is the value of the next positional argument, not the value for `-Switch`. + +### Combined switch arguments + +For switch arguments with short names when using long/short mode, the switches can be combined in a +single argument. For example, given the switches with the short names `-a`, `-b` and `-c`, the +following command line sets all three switches: + +```text +-abc +``` + +This is equivalent to: + +```text +-a -b -c +``` -If you use a nullable Boolean type (`bool?`) as the type of the argument, it will be `null` if -not supplied, `true` if supplied, and `false` only if explicitly set to false using `-Switch:false`. +This only works for switch arguments, and does not apply to long names or the default parsing mode. ## Arguments with multiple values @@ -128,15 +195,16 @@ In this case, if `-ArgumentName` is a multi-value argument, the value of the arg holding all three values. It’s possible to specify a separator for multi-value arguments using the -[`MultiValueSeparatorAttribute`][] attribute. This makes it possible to specify multiple values for the -argument while the argument itself is specified only once. For example, if the separator is set to a -comma, you can specify the values as follows: +[`MultiValueSeparatorAttribute`][] attribute. This makes it possible to specify multiple values for +the argument while the argument itself is specified only once. For example, if the separator is set +to a comma, you can specify the values as follows: ```text -ArgumentName value1,value2,value3 ``` -In this case, the value of the argument named `-ArgumentName` will be a list with the three values "value1", "value2" and "value3". +In this case, the value of the argument named `-ArgumentName` will be a list with the three values +"value1", "value2" and "value3". **Note:** if you specify a separator for a multi-value argument, it is _not_ possible to have an argument value containing the separator. There is no way to escape the separator. Therefore, make @@ -161,9 +229,8 @@ positional argument values will be considered values for the multi-value argumen If a multi-value argument is required, it means it must have at least one value. You cannot set a default value for an optional multi-value argument. -If the type of the argument is a list of Boolean values (e.g. `bool[]`), it will act as a -multi-value argument and a switch. A value of true (or the explicit value if one is given) gets -added to the list for every time that the argument is supplied. +An argument can be both multi-value and a switch. A value of true (or the explicit value if one is +given) gets added to the list for every time that the argument is supplied. If an argument is not a multi-value argument, it is an error to supply it more than once, unless duplicate arguments are allowed in the [`ParseOptions`][] or [`ParseOptionsAttribute`][], in which @@ -185,7 +252,8 @@ In this case, the value of the argument named `-ArgumentName` will be a dictiona If you specify the same key more than once, an exception will be thrown, unless the [`AllowDuplicateDictionaryKeysAttribute`][] attribute is specified for the argument. -The default key/value separator (which is `=`) can be overridden using the [`KeyValueSeparatorAttribute`][] attribute. +The default key/value separator (which is `=`) can be overridden using the +[`KeyValueSeparatorAttribute`][] attribute. ## Argument value conversion @@ -197,30 +265,37 @@ type. Ookii.CommandLine will try to convert the argument using the following options, in order of preference: -1. If the argument has the [`TypeConverterAttribute`][] applied, the specified custom - [`TypeConverter`][]. -2. The argument type's default [`TypeConverter`][], if it can convert from a string. -3. A `public static Parse(String, ICultureInfo)` method. -4. A `public static Parse(String)` method. -5. A public constructor that takes a single string argument. +1. If the argument has the [`ArgumentConverterAttribute`][] applied, the specified custom + [`ArgumentConverter`][]. +2. For .Net 7 and later: + 1. An implementation of the [`ISpanParsable`][] interface. + 2. An implementation of the [`IParsable`][] interface. +3. A `public static Parse(string, ICultureInfo)` method. +4. A `public static Parse(string)` method. +5. A public constructor that takes a single `string` argument. This will cover the majority of types you'd want to use for arguments without having to write any conversion code. If you write your own custom type, you can use it for arguments as long as it meets -one of the above criteria (a [`TypeConverter`][] is preferred). +one of the above criteria. + +It is possible to override the default conversion by specifying a custom converter using the +[`ArgumentConverterAttribute`][]. When this attribute is applied to an argument, the specified type +converter will be used for conversion instead of any of the default methods. -It is possible to override the default conversion by specifying a custom type converter using the -[`System.ComponentModel.TypeConverterAttribute`][]. When this attribute is applied to an argument, -the specified type converter will be used for conversion instead of any of the default methods. +Previous versions of Ookii.CommandLine used .Net's [`TypeConverter`][] class. Starting with +Ookii.CommandLine 4.0, this is no longer the case, and the [`ArgumentConverter`][] class is used +instead. [See here](DefiningArguments.md#custom-type-conversion) for more information on how to +upgrade code that relied on a [`TypeConverter`][]. -### Enumeration type conversion +### Enumeration conversion -The default [`TypeConverter`][] for enumeration types 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. 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`][]. -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` +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 @@ -245,20 +320,23 @@ the use of numeric values entirely. ### Multi-value and dictionary value conversion For multi-value and dictionary arguments, the converter must be for the element type (e.g. if the -argument is a multi-value argument of type `int[]`, the type converter must be able to convert to -`int`). For a dictionary argument the element type is [`KeyValuePair`][], and the type +argument is a multi-value argument of type `int[]`, the argument converter must be able to convert to +`int`). + +For a dictionary argument the element type is [`KeyValuePair`][], and the type converter is responsible for parsing the key and value from the argument value. Ookii.CommandLine provides the [`KeyValuePairConverter`][] class that is used by default -for dictionary arguments. You can override this using the [`TypeConverterAttribute`][] as usual, but +for dictionary arguments. You can override this using the [`ArgumentConverterAttribute`][] as usual, but if you only want to customize the parsing of the key and value types, you can use the -[`KeyTypeConverterAttribute`][] and the [`ValueTypeConverterAttribute`][] attributes respectively. -The [`KeyValuePairConverter`][] will use those attributes to locate a custom converter. -You can also customize the key/value separator used by this converter using the -[`KeyValueSeparatorAttribute`][] attribute. +[`KeyConverterAttribute`][] and the [`ValueConverterAttribute`][] attributes respectively. + +The [`KeyValuePairConverter`][] will use those attributes to determine which converter to +use instead of the default for the key and value types. You can also customize the key/value +separator used by this converter using the [`KeyValueSeparatorAttribute`][] attribute. -If you do specify the [`TypeConverterAttribute`][] for a dictionary argument, the -[`KeyTypeConverterAttribute`][], [`ValueTypeConverterAttribute`][], and [`KeyValueSeparatorAttribute`][] +If you do specify the [`ArgumentConverterAttribute`][] for a dictionary argument, the +[`KeyConverterAttribute`][], [`ValueConverterAttribute`][], and [`KeyValueSeparatorAttribute`][] attributes will be ignored. ### Conversion culture @@ -269,122 +347,97 @@ interpreted; for example, some cultures might use a period as the decimal separa use a comma. To ensure a consistent parsing experience for all users regardless of their machine's regional -format settings, Ookii.CommandLine defaults to using [`CultureInfo.InvariantCulture`][]. You can change -this using the [`ParseOptions.Culture`][] property, but be very careful if you do. +format settings, Ookii.CommandLine defaults to using [`CultureInfo.InvariantCulture`][]. You can +change this using the [`ParseOptions.Culture`][] property, but be very careful if you do. ## Arguments with non-nullable types Ookii.CommandLine provides support for nullable reference types. Not only is the library itself -fully annotated, but if you use the .Net 6.0 version of the library, command line argument parsing -takes into account the nullability of the argument types. If the argument is declared with a -nullable reference or value type (e.g. `string?` or `int?`), nothing changes. But if the argument is -not nullable (e.g. `string` (in a context with NRT support) or `int`), [`CommandLineParser`][] will -ensure that the value will not be null. +fully annotated, but if you use [source generation](SourceGeneration.md) or the .Net 6.0 version of +the library, command line argument parsing takes into account the nullability of the argument types. +If the argument is declared with a nullable reference or value type (e.g. `string?` or `int?`), +nothing changes. But if the argument is not nullable (e.g. `string` (in a context with NRT support) +or `int`), [`CommandLineParser`][] will ensure that the value will not be null. -Assigning a null value to an argument only happens if the [`TypeConverter`][] for that argument returns -`null` as the result of the conversion. If this happens and the argument is not nullable, a -[`CommandLineArgumentException`][] is thrown with the category set to [`NullArgumentValue`][NullArgumentValue_0]. - -Null-checking for non-nullable reference types is only available in .Net 6.0 and later. If you are -using the .Net Standard versions of Ookii.CommandLine, this check is only done for value types. +Assigning a null value to an argument only happens if the [`ArgumentConverter`][] for that argument +returns null as the result of the conversion. If this happens and the argument is not nullable, a +[`CommandLineArgumentException`][] is thrown with the category set to +[`NullArgumentValue`][NullArgumentValue_0]. For multi-value arguments, the nullability check applies to the type of the elements (e.g. `string?[]` for an array), and for dictionary arguments, it applies to the value (e.g. -`Dictionary`); the key may never be null for a dictionary argument. +`Dictionary`); the key may never be nullable for a dictionary argument. + +Null-checking for non-nullable reference types is available for all runtime versions if you use +source generation. If you cannot use source generation, only the .Net 6.0 and later versions of +Ookii.CommandLine can determine the nullability of reference types when using reflection. The +.Net Standard versions of the library will only apply this check to value types unless source +generation was used. See also the [`CommandLineArgument.AllowNull`][] property. ## Long/short mode -POSIX and GNU conventions specify that options use a dash (`-`) followed by a single characters, and -define the concept of long options, which use `--` followed by an a multi-character name. This style -is used by many tools like `dotnet`, `git`, and many others, and may be preferred if you are writing -a cross-platform application. +The default behavior of Ookii.CommandLine is similar to how PowerShell parses arguments. However, +many command line tools like `dotnet`, `git`, and many others use POSIX or GNU conventions. This is +especially common for Linux or cross-platform applications. + +POSIX and GNU conventions specify that options use a dash (`-`) followed by a single character, and +define the concept of long options, which use `--` followed by an a multi-character name. Ookii.CommandLine calls this style of parsing "long/short mode," and offers it as an alternative -mode to augment the default parsing rules. In this mode, an argument can have the regular long name -and an additional single-character short name, each with its own argument name prefix. By default, -the prefix `--` is used for long names, and `-` (and `/` on Windows) for short names. +mode to the default parsing rules. In this mode, an argument can have a long name, which takes the +place of the regular argument name, and an additional single-character short name. By default, +Ookii.CommandLine follows the convention of using the prefix `--` for long names, and `-` (and `/` +on Windows only) for short names. + +Besides allowing the alternative names, long/short mode follows the same rules as the default mode, +with the differences as explained above. POSIX conventions also specify the use of lower case argument names, with dashes separating words ("dash-case"), which you can easily achieve using [name transformation](DefiningArguments.md#name-transformation), -and case-sensitive argument names, which can be enabled with the -[`ParseOptionsAttribute.CaseSensitive`][] property or the [`ParseOptions.ArgumentNameComparer`][] -property. - -For example, an argument named `--path` could have a short name `-p`. It could then be supplied -using either name: - -```text ---path value -``` - -Or: - -```text --p value -``` - -Note that you must use the correct prefix: using `-path` or `--p` will not work. - -An argument can have either a short name or a long name, or both. - -Arguments in this mode can still have aliases. You can set separate long and short aliases, which -follow the same rules as the long and short names. - -For switch arguments with short names, the switches can be combined in a single argument. For -example, given the switches `-a`, `-b` and `-c`, the following command line sets all three switches: - -```text --abc -``` - -This is equivalent to: - -```text --a -b -c -``` - -This only works for switch arguments, and does not apply to long names. - -Besides these differences, long/short mode follows the same rules and conventions as the default -mode outlined above, with all the same options. +and case-sensitive argument names. For information on how to set these options, +[see here](DefiningArguments.md#longshort-mode). ## More information Next, let's take a look at how to [define arguments](DefiningArguments.md). -[`AllowDuplicateDictionaryKeysAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute.htm -[`CommandLineArgument.AllowNull`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgument_AllowNull.htm -[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentException.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`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 [`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 [`DayOfWeek.Monday`]: https://learn.microsoft.com/dotnet/api/system.dayofweek [`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 [`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 -[`KeyTypeConverterAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_KeyTypeConverterAttribute.htm +[`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 [`KeyValuePair`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.keyvaluepair-2 -[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_KeyValuePairConverter_2.htm -[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_KeyValueSeparatorAttribute.htm -[`MultiValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_MultiValueSeparatorAttribute.htm -[`ParseOptions.AllowWhiteSpaceValueSeparator`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator.htm -[`ParseOptions.ArgumentNameComparer`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparer.htm -[`ParseOptions.Culture`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_Culture.htm -[`ParseOptions.NameValueSeparator`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_NameValueSeparator.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute.AllowWhiteSpaceValueSeparator`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_AllowWhiteSpaceValueSeparator.htm -[`ParseOptionsAttribute.CaseSensitive`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm -[`ParseOptionsAttribute.NameValueSeparator`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparator.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.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 +[`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 [`String`]: https://learn.microsoft.com/dotnet/api/system.string -[`System.ComponentModel.TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter -[`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri -[`ValueTypeConverterAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ValueTypeConverterAttribute.htm -[NullArgumentValue_0]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`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 diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 7e96b9c4..cccacd8e 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,13 +1,62 @@ # What’s new in Ookii.CommandLine -**IMPORTANT:** If you are upgrading from version 2.x, please check the [migration guide](Migrating.md). - -## Ookii.CommandLine 3.1.1 +## Ookii.CommandLine 4.0 - 2023-07-20 + +**IMPORTANT:** Version 4.0 contains breaking changes. If you are upgrading from version 2.x or 3.x, +please check the [migration guide](Migrating.md). + +- Add support for [source generation](SourceGeneration.md). + - Use the [`GeneratedParserAttribute`][] to determine command line arguments at compile time. + - Get errors and warnings for many mistakes. + - Automatically determine the order of positional arguments. + - Use property initializers to set default values that are used in the usage help. + - Allow your application to be trimmed. + - Improved performance. + - Use the [`GeneratedCommandManagerAttribute`][] to determine subcommands at compile time. + - Allow an application with subcommands to be trimmed. + - Improved performance. + - Using source generation is recommended unless you are not able to meet the requirements. +- Constructor parameters can no longer be used to define command line arguments. +- Converting strings to argument types is now done using Ookii.CommandLine's own [`ArgumentConverter`][] + class. + - See the [migration guide](Migrating.md) for more information. + - This enables conversion using [`ReadOnlySpan`][] for better performance, makes it easier to + implement new converters, provides better error messages for enumeration conversion, and enables + the use of trimming (when source generation is used). + - For .Net 7 and later, support value conversion using the [`ISpanParsable`][] and + [`IParsable`][] interfaces. +- Automatically accept [any unique prefix](DefiningArguments.md#automatic-prefix-aliases) of an + argument name as an alias. +- Use the `required` keyword in C# 11 and .Net 7.0 to create required arguments. +- Support for using properties with `init` accessors (only if they are `required`). +- Value descriptions are now specified using the [`ValueDescriptionAttribute`][] attribute. This + attribute is not sealed to allow derived classes that implement localization. +- Conveniently set several related options to enable POSIX-like conventions using the + [`ParseOptions.IsPosix`][], [`CommandOptions.IsPosix`][] or [`ParseOptionsAttribute.IsPosix`][] property. +- Support for multiple argument name/value separators, with the default now accepting both `:` and + `=`. +- You can now [cancel parsing](DefiningArguments.md#arguments-that-cancel-parsing) and still return + success. +- The remaining unparsed arguments, if parsing was canceled or encountered an error, are available + through the [`CommandLineParser.ParseResult`][] property. +- Argument validators used before conversion can implement validation on [`ReadOnlySpan`][] for + better performance. +- Built-in support for [nested subcommands](Subcommands.md#nested-subcommands). +- The automatic version argument and command will use the [`AssemblyTitleAttribute`][] if the + [`ApplicationFriendlyNameAttribute`][] was not used. +- By default, only usage syntax is shown if a parsing error occurs; the help argument must be used + to get full help. +- Exclude the default value from the usage help on a per argument basis with the + [`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`][] property. +- [Source link](https://github.com/dotnet/sourcelink) integration. +- Various bug fixes and minor improvements. + +## Ookii.CommandLine 3.1.1 - 2023-03-29 - .Net Standard 2.0: use the System.Memory package to remove some downlevel-only code. - There are no changes for the .Net Standard 2.1 and .Net 6.0 assemblies. -## Ookii.CommandLine 3.1 +## Ookii.CommandLine 3.1 - 2023-03-21 - Added an instance [`CommandLineParser.ParseWithErrorHandling()`][] method, which handles errors and displays usage help the same way as the static [`Parse()`][Parse()_1] method, but allows access to more @@ -30,7 +79,7 @@ writer yet. - Some minor bug fixes. -## Ookii.CommandLine 3.0 +## Ookii.CommandLine 3.0 - 2022-12-01 **IMPORTANT:** Several of the changes in version 3.0 are *breaking changes*. There are breaking API changes as well as several behavior changes. In general, it's not expected that you'll need to make @@ -64,8 +113,8 @@ existing application. - Optional support for [multi-value arguments](Arguments.md#arguments-with-multiple-values) that consume multiple argument tokens without a separator, e.g. `-Value 1 2 3` to assign three values. - - Arguments classes can [use a constructor parameter](DefiningArguments.md#commandlineparser-injection) - to receive the [`CommandLineParser`][] instance they were created with. + - Arguments classes can [use a constructor parameter](DefiningArguments.md) to receive the + [`CommandLineParser`][] instance they were created with. - Added the ability to customize error messages and other strings. - Subcommands - Renamed "shell commands" to "subcommands" because I never liked the old name. @@ -95,7 +144,7 @@ existing application. - No longer targets .Net Framework 2.0 - Now targets .Net Standard 2.0, .Net Standard 2.1, and .Net 6.0 and later. -## Ookii.CommandLine 2.4 +## Ookii.CommandLine 2.4 - 2022-09-01 - Ookii.CommandLine now comes in a .Net 6.0 version that fully supports nullable reference types (.Net Framework 2.0 and .Net Standard 2.0 versions are also still provided). @@ -107,18 +156,18 @@ existing application. - Arguments can indicate they cancel parsing to make adding a `-Help` or `-?` argument easier. - Some small bug fixes. -## Ookii.CommandLine 2.3 +## Ookii.CommandLine 2.3 - 2019-09-05 - Ookii.CommandLine now comes in both a .Net Framework 2.0 and .Net Standard 2.0 version. -## Ookii.CommandLine 2.2 +## Ookii.CommandLine 2.2 - 2013-02-06 - Added support for alternative names (aliases) for command line arguments. - An argument’s aliases and default value can be included in the argument description when generating usage. - Added code snippets. -## Ookii.CommandLine 2.1 +## Ookii.CommandLine 2.1 - 2012-02-19 - Added support for dictionary arguments; these are special multi-value arguments whose values take the form key=value. @@ -138,7 +187,7 @@ existing application. - Shell commands can use custom argument parsing. - Various minor bug fixes. -## Ookii.CommandLine 2.0 +## Ookii.CommandLine 2.0 - 2011-08-13 - Improved argument parsing: - All arguments can be specified by name. @@ -165,25 +214,38 @@ 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. -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser_1.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 +[`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 [`CultureInfo.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.invariantculture [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs -[`ParseOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm -[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter -[Parse()_6]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[UsageWriter_1]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm -[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm -[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm -[`LineWrappingTextWriter.ToString()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ToString.htm -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm -[`ResetIndentAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndentAsync.htm +[`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 +[`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 +[`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 [`StringWriter`]: https://learn.microsoft.com/dotnet/api/system.io.stringwriter -[`Wrapping`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm -[Flush()_0]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_Flush_1.htm -[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm -[WriteAsync()_4]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync.htm -[WriteLineAsync()_5]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteLineAsync.htm +[`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 diff --git a/docs/CodeSnippets.md b/docs/CodeSnippets.md index c6470df7..fc6ea4a3 100644 --- a/docs/CodeSnippets.md +++ b/docs/CodeSnippets.md @@ -3,17 +3,17 @@ Several code snippets for Visual Studio are provided to make working with Ookii.CommandLine even easier: -- **clargclass:** Creates an arguments class, including a static `Parse` method that parses - arguments. +- **clargclass:** Creates an arguments class. - **clarg:** Creates a property for a command line argument. -- **clargpos:** Creates a property for a positional and optionally required command line argument. +- **clargpos:** Creates a property for a positional command line argument. +- **clargreq:** Creates a property for a required positional command line argument (C# only). - **clargmulti:** Creates a property for a multi-value command line argument. - **clargdict:** Creates a property for a dictionary command line argument. - **clargswitch:** Creates a property for a switch argument. - **clcmd:** Creates a subcommand class. - **clcmdasync:** Creates an asynchronous subcommand class. -All snippets are provided for C# and Visual Basic. +All snippets are provided for C# and Visual Basic, except as noted. A [Visual Studio extension](https://www.ookii.org/Link/CommandLineSnippets) is provided that installs the snippets. diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 3142bec5..5658ddbf 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -7,22 +7,37 @@ To define which arguments are accepted by your application, you create a class w the names, types and other attributes (such as whether they're required or positional) of the arguments. -There are three ways to define arguments in the class: using properties, methods, and constructor -parameters. +The class itself has no special requirements, but will typically look like this. + +```csharp +[GeneratedParser] +partial class Arguments +{ +} +``` + +The use of the [`GeneratedParserAttribute`][] enables [source generation](SourceGeneration.md), +which has several advantages and should be used unless you cannot meet the requirements. + +The class must have a public constructor with no parameters, or one that takes a single +[`CommandLineParser`][] parameters. If the latter is used, the [`CommandLineParser`][] instance that +was used to parse the arguments will be passed to the constructor. + +There are two ways to define arguments in the class: using properties and using methods. ## Using properties -Properties are the most flexible way to define arguments. They can be used to create any type of -argument, and lend themselves well to using attributes without the code becoming cluttered. +Properties are the most common way to define arguments. They can be used to create any type of +argument, and will be used for most arguments. -To indicate a property is an argument, apply the [`CommandLineArgumentAttribute`] attribute to it. +To indicate a property is an argument, apply the [`CommandLineArgumentAttribute`][] attribute to it. The property must have a public getter and setter, except for multi-value and dictionary arguments which can be defined by read-only properties. The type of the property is used for the type of the argument, and the name of the property is used as the argument name by default. -If not specified otherwise, a property defines an optional and not positional. +If not specified otherwise, the argument will be optional and not positional. The below defines an argument with the name `-SomeArgument`. Its type is a [`String`][], it's optional, can only be specified by name, and has no default value: @@ -33,9 +48,10 @@ public string? SomeArgument { get; set; } ``` > All examples on this page assume you are using the default parsing mode (not long/short) and no -> name transformation, unless specified otherwise. +> name transformation, unless specified otherwise. With the [right options](#longshort-mode), this +> same property could also define an argument called `--some-argument`. -If you don't want to use the name of the property (and a [name transformation](#name-transformation)) +If you don't want to use the name of the property (and a [name transformation](#name-transformation) is not appropriate), you can specify the name explicitly. ```csharp @@ -45,22 +61,42 @@ public string? SomeArgument { get; set; } This creates an argument named `-OtherName`. -### Required and positional arguments +### Positional arguments + +There are two ways to make an argument positional. -To create a required argument, set the [`CommandLineArgumentAttribute.IsRequired`][] property to -true. To create a positional argument, set the [`CommandLineArgumentAttribute.Position`][] property -to a non-negative number. +When using [source generation](SourceGeneration.md), you can use the [`CommandLineArgumentAttribute.IsPositional`][] +property. With this option, the arguments will have the same order as the members that define them. ```csharp -[CommandLineArgument(Position = 0, IsRequired = true)] +[CommandLineArgument(IsPositional = true)] +public string? SomeArgument { get; set; } + +[CommandLineArgument(IsPositional = true)] public int OtherArgument { get; set; } ``` -This defines a required positional argument named `-OtherArgument`. +Here, `-SomeArgument` will be the first positional argument, and `-OtherArgument` the second. + +If not using source generation, you must instead set the [`CommandLineArgumentAttribute.Position`][] +property to a non-negative number. The numbers determine the order. + +> Without source generation, reflection is used to determine the arguments, and reflection is not +> guaranteed to return the members of a type in any particular order, which is why the +> [`IsPositional`][] property is only supported when using source generation. The [`Position`][Position_1] property +> works with both source generation and reflection. + +```csharp +[CommandLineArgument(Position = 0)] +public string? SomeArgument { get; set; } + +[CommandLineArgument(Position = 1)] +public int OtherArgument { get; set; } +``` The [`CommandLineArgumentAttribute.Position`][] property specifies the relative position of the arguments, not their actual position. Therefore, it's okay to skip numbers; only the order matters. -The order of the properties themselves does not matter. +The order of the properties themselves does not matter in this case. That means that this: @@ -88,9 +124,39 @@ public int Argument2 { get; set; } public int Argument1 { get; set; } ``` +And is also equivalent to this when using the [`GeneratedParserAttribute`][]: + +```csharp +[CommandLineArgument(IsPositional = true)] +public int Argument1 { get; set; } + +[CommandLineArgument(IsPositional = true)] +public int Argument2 { get; set; } + +[CommandLineArgument(IsPositional = true)] +public int Argument3 { get; set; } +``` + +### Required arguments + +To create a required argument, use a `required` property (.Net 7 and later only), or set the +[`CommandLineArgumentAttribute.IsRequired`][] property to true. It's recommended for required +properties to also be positional. + +```csharp +[CommandLineArgument(IsPositional = true)] +public required string SomeArgument { get; set; } + +[CommandLineArgument(IsPositional = true, IsRequired = true)] +public int OtherArgument { get; set; } +``` + +Here, both `-SomeArgument` and `-OtherArgument` are required and positional. + You cannot define a required positional argument after an optional positional argument, and a multi-value positional argument must be the last positional argument. If your properties violate -these rules, the [`CommandLineParser`][] class’s constructor will throw an exception. +these rules, you will get a compile time error when using source generation, and if not, the +[`CommandLineParser`][] class’s constructor will throw an exception. ### Switch arguments @@ -115,8 +181,8 @@ explicitly set to true with `-Switch:true`, and false only if the user supplied ### Multi-value arguments -There are two ways to define multi-value arguments using properties. The first is to use a -read-write property of any array type: +There are two ways to define multi-value arguments. The first is to use a read-write property of any +array type: ```csharp [CommandLineArgument] @@ -127,23 +193,36 @@ Note that if no values are supplied, the property will not be set, so it can be If the property has an initial non-null value, that value will be overwritten if the argument was supplied. -The other option is to a read-only property of any type implementing [`ICollection`][] (e.g. [`List`][]). This requires that the property's value is not null, and items will be added -to the list after parsing has completed. +The other option is to use a read-only property of any type implementing [`ICollection`][] (e.g. +[`List`][]). This requires that the property's value is not null, and items will be added to +the list after parsing has completed. ```csharp [CommandLineArgument] -public ICollection AlsoMultiValue { get; } = new List(); +public List AlsoMultiValue { get; } = new(); ``` -It is possible to use [`List`][] (or any other type implementing [`ICollection`][]) as -the type of the property itself, but, if using .Net 6.0 or later, [`CommandLineParser`][] can only -determine the [nullability](Arguments.md#arguments-with-non-nullable-types) of the collection's -elements if the property type is either an array or [`ICollection`][] itself. +If you are _not_ using the [`GeneratedParserAttribute`][] attribute, using .Net 6.0 or later, and +using a read-only property like this, it is recommended to use [`ICollection`][] as the type of +the property. Otherwise, [`CommandLineParser`][] will not be able to determine the +[nullability](Arguments.md#arguments-with-non-nullable-types) of the collection's elements. This +limitation does not apply to source generation. + +A multi-value argument whose type is a boolean or a nullable boolean is both a switch and a +multi-value argument. + +```csharp +[CommandLineArgument] +public bool[] Switch { get; set; } +``` + +A value of true, or the explicit value, will be added to the array for each time the argument is +supplied. ### Dictionary arguments -Similar to array arguments, there are two ways to define dictionary arguments: a read-write property -of type [`Dictionary`][], or a read-only property of any type implementing +Similar to multi-value arguments, there are two ways to define dictionary arguments: a read-write +property of type [`Dictionary`][], or a read-only property of any type implementing [`IDictionary`][]. When using a read-write property, the property value may be null if the argument was not supplied, @@ -155,16 +234,34 @@ arguments. public Dictionary? Dictionary { get; set; } [CommandLineArgument] -public IDictionary AlsoDictionary { get; } = new SortedDictionary(); +public SortedDictionary AlsoDictionary { get; } = new(); ``` -As above, it is possible to use the actual type (in this case, [`SortedDictionary`][]) as -the property type for the second case, but nullability for the dictionary values can only be -determined if the type is [`Dictionary`][] or [`IDictionary`][]. +As above, when using a read-only property and not using the [`GeneratedParserAttribute`][] +attribute, you should use either [`Dictionary`][] or [`IDictionary`][] +as the type of the property, otherwise the nullability of `TValue` cannot be determined. ### Default values -For an optional argument, you can specify the default value using the +There are two ways to set default values for an optional argument. The first is to use a property +initializer: + +```csharp +[CommandLineArgument] +public string SomeArgument { get; set; } = "default"; +``` + +If the argument is not supplied, the property will have its initial value, which is "default" in +this case. + +When using source generation, the value of the property initializer will be included in the +argument's description in the [usage help](UsageHelp.md) as long as the value is either a literal, a +constant, a property reference, or an enumeration value. Other types of initializers (such as a +`new` expression or a method call), will not have their value shown in the usage help. + +> You can disable showing default values in the usage help if you do not want it. + +Alternatively, you can specify the default value using the [`CommandLineArgumentAttribute.DefaultValue`][] property. ```csharp @@ -172,37 +269,21 @@ For an optional argument, you can specify the default value using the public int SomeArgument { get; set; } ``` -The default value must be either the type of the argument, or a type that can be converted to the -argument type. Since all argument types must be convertible from a string, this enables you to use -strings for types that don't have literals. +The [`DefaultValue`][] property must use either the type of the argument, or a string that can be +converted to the argument type. This enables you to set a default value for types that don't have +literals. ```csharp [CommandLineArgument(DefaultValue = "1969-07-20")] public DateOnly SomeArgument { get; set; } ``` -The default value is used if an optional argument is not supplied; in that case -[`CommandLineParser`][] will set the property to the specified default value. - The value of the [`CommandLineArgumentAttribute.DefaultValue`][] property will be included in the -argument's description in the [usage help](UsageHelp.md) by default, so you don't need to manually -duplicate the value in your description. +argument's description in the [usage help](UsageHelp.md). In this case, it will be included +regardless of whether you are using source generation. -If no default value is specified (the value is null), the [`CommandLineParser`][] will never set the -property if the argument was not supplied. This means that if you initialized the property to some -value, this value will not be changed. - -```csharp -[CommandLineArgument] -public string SomeProperty { get; set; } = "default"; -``` - -Here, the property's value will remain "default" if the argument was not specified. This can be -useful if the argument uses a [non-nullable reference type](Arguments.md#arguments-with-non-nullable-types), -which must be initialized with a non-null value. - -When using this method, the property's initial value will not be included in the usage help, so you -must include it manually if desired. +Default values will be ignored if specified for a required argument or a multi-value or dictionary +argument. ### Argument descriptions @@ -226,63 +307,103 @@ the descriptions, this can be accomplished by creating a class that derives from The value description is a short, often one-word description of the type of values your argument accepts. It's shown in the [usage help](UsageHelp.md) after the name of your argument, and defaults to the name of the argument type (in the case of a multi-value argument, the element type, or for a -nullable value type, the underlying type). +nullable value type, the underlying type). The unqualified framework type name is used, so for +example, an integer would have the default value description "Int32". -To specify a custom value description, use the [`CommandLineArgumentAttribute.ValueDescription`][] -property. +To specify a custom value description, use the [`ValueDescriptionAttribute`][] attribute. ```csharp -[CommandLineArgument(ValueDescription = "Number")] +[CommandLineArgument] +[ValueDescriptionAttribute("Number")] public int Argument { get; set; } ``` -This should *not* be used for the description of the argument's purpose; use the +This should _not_ be used for the description of the argument's purpose; use the [`DescriptionAttribute`][] for that. ### Custom type conversion If you want to use a non-default conversion from string, you can specify a custom type converter -using the [`TypeConverterAttribute`][]. +using the [`ArgumentConverterAttribute`][]. ```csharp [CommandLineArgument] -[TypeConverter(typeof(CustomConverter))] +[ArgumentConverter(typeof(CustomConverter))] public int Argument { get; set; } ``` -The type specified must be derived from the [`TypeConverter`][] class. +The type specified must be derived from the [`ArgumentConverter`][] class. + +To create a custom converter, create a class that derives from the [`ArgumentConverter`][] class. +Argument conversion can use either a [`ReadOnlySpan`][] or a [`String`][], and it's recommended to +support the [`ReadOnlySpan`][] method to avoid unnecessary string allocations. + +Previous versions of Ookii.CommandLine used .Net's [`TypeConverter`][] class. Starting with +Ookii.CommandLine 4.0, this is no longer the case, and the [`ArgumentConverter`][] class is used +instead. + +To help with transitioning code that relied on [`TypeConverter`][], you can use the +[`WrappedDefaultTypeConverter`][] class to use a type's default type converter. + +```csharp +[CommandLineArgument] +[ArgumentConverter(typeof(WrappedDefaultTypeConverter))] +public SomeType Argument { get; set; } +``` + +This will use [`TypeDescriptor.GetConverter()`][] function to get the default [`TypeConverter`][] for +the type. Note that using that function will make it impossible to trim your application; this is +the main reason [`TypeConverter`][] is no longer the default for converting arguments. -To make it easy to implement custom type converters to/from a string, Ookii.CommandLine provides -the [`TypeConverterBase`][] type. +If you were using a custom [`TypeConverter`][], you can use the [`WrappedTypeConverter`][] class. ### Arguments that cancel parsing -You can indicate that argument parsing should stop and immediately print usage help when an argument -is supplied by setting the [`CommandLineArgumentAttribute.CancelParsing`][] property to true. +You can indicate that argument parsing should stop immediately when an argument is supplied by +setting the [`CommandLineArgumentAttribute.CancelParsing`][] property. -When this property is set, parsing is stopped when the argument is encountered. The rest of the -command line is not processed, and [`CommandLineParser.Parse()`][CommandLineParser.Parse()_2] will -return null. The [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] and the static [`Parse()`][Parse()_1] helper -methods will automatically print usage in this case. +When this property is set to [`CancelMode.Abort`][], parsing is stopped when the argument is +encountered. The rest of the command line is not processed, and +[`CommandLineParser.Parse()`][] will return null. The +[`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] and the static [`Parse()`][Parse()_1] +helper methods will automatically print usage in this case. -This can be used to implement a custom `-Help` argument, if you don't wish to use the default one. +This can be used, for example, to implement a custom `-Help` argument, if you don't wish to use the +default one. ```csharp -[CommandLineArgument(CancelParsing = true)] +[CommandLineArgument(CancelParsing = CancelMode.Abort)] 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. + +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 +encountered an error. + +[`CancelMode.Success`][] can be used if you wish to pass the remaining arguments to another command +line processor, for example a child application, or a subcommand. See for example the +[top-level arguments sample](../src/Samples/TopLevelArguments). + ## Using methods -You can also apply the [`CommandLineArgumentAttribute`][] to a method. Method arguments offer a way -to take action immediately if an argument is supplied, without waiting for the remaining arguments -to be parsed. +You can also apply the [`CommandLineArgumentAttribute`][] to a public static method. Method +arguments offer a way to take action immediately if an argument is supplied, without waiting for the +remaining arguments to be parsed. The method must have one of the following signatures. +- `public static CancelMode Method(ArgumentType value, CommandLineParser parser);` +- `public static CancelMode Method(ArgumentType value);` +- `public static CancelMode Method(CommandLineParser parser);` +- `public static CancelMode Method();` - `public static bool Method(ArgumentType value, CommandLineParser parser);` - `public static bool Method(ArgumentType value);` - `public static bool Method(CommandLineParser parser);` @@ -298,186 +419,82 @@ hasn't been created yet when the method is invoked. The type of the `value` parameter is the type of the argument. If the method doesn't have a `value` parameter, the argument will be a switch argument, and the method will be invoked when the argument -is supplied, even if its value is explicitly set to false. +is supplied, even if its value is explicitly set to false (if you want to distinguish this, use +a `bool value` parameter). Multi-value method arguments are not supported, so the type of the `value` parameter may not be an -array, collection or dictionary type. +array, collection or dictionary type, unless you provide an [`ArgumentConverter`][] that can convert +to that type. -If you use one of the signatures with a `bool` return type, returning false will cancel parsing. -Unlike the [`CancelParsing`][CancelParsing_1] property, this will *not* automatically display usage -help. If you do want to show help, set the [`CommandLineParser.HelpRequested`][] property to true -before returning false. +If you use one of the signatures with a [`CancelMode`][] return type, returning [`CancelMode.Abort`][] or +[`CancelMode.Success`][] will immediately [cancel parsing](#arguments-that-cancel-parsing). Unlike the +[`CancelParsing`][CancelParsing_1] property, [`CancelMode.Abort`][] will _not_ automatically display +usage help. If you do want to show help, set the [`CommandLineParser.HelpRequested`][] property to +true before returning false. ```csharp [CommandLineArgument] -public static bool MoreHelp(CommandLineParser parser) +public static CancelMode MoreHelp(CommandLineParser parser) { Console.WriteLine("Some amazingly useful information.") parser.HelpRequested = true; - return false; + return CancelMode.Abort; } ``` -Method arguments allow all the same customizations as property-defined arguments, except that the -[`DefaultValue`][DefaultValue_1] will not be used. The method will never be invoked if the argument is not explicitly -specified by the user. - -## Using constructor parameters - -An alternative way to define arguments is using a public constructor on your arguments class. These -arguments will be positional arguments, and required unless the constructor parameter is optional. - -The following creates a required positional argument named `-arg1`, a required positional argument -named `-arg2`, and an optional positional argument named `-arg3`, with a default value of 0 (which -will be included in the usage help). - -```csharp -public class MyArguments -{ - public MyArguments(string arg1, int arg2, float arg3 = 0f) - { - /* ... */ - } -} -``` - -Arguments defined by constructor parameters will always be positional, with their order matching the -order of the parameters. If there are properties defining positional arguments, those will always -come after the arguments defined by the constructor. - -```csharp -public class MyArguments -{ - public MyArguments(string arg1, int arg2, float arg3 = 0f) - { - /* ... */ - } - - [CommandLineArgument(Position = 0)] - public int PropertyArg { get; set; } -} -``` - -In this case, `-PropertyArg` will be the fourth positional argument. - -You cannot use the [`CommandLineArgumentAttribute`] on a constructor parameter, so things that -are normally specified this way are specified using other attributes. The [`ArgumentNameAttribute`][] -is used if you want an argument name different than the parameter name. It can also be used to set -the short name for [long/short mode](Arguments.md#longshort-mode). +When using a signature that returns `bool`, returning `true` is equivalent to [`CancelMode.None`][] and +`false` is equivalent to [`CancelMode.Abort`][]. -The [`ValueDescriptionAttribute`][] is used to set a custom value description, and full descriptions -are still set using the [`DescriptionAttribute`][]. +Using a signature that returns `void` is equivalent to returning [`CancelMode.None`][]. -```csharp -public MyArguments( - [ArgumentName("Count", IsShort = true)] - [ValueDescription("Number")], - [Description("Provides a count to the application.")] - int count) -{ - /* ... */ -} -``` - -As you can see, it becomes rather awkward to use all these attributes on constructor parameters, -which is why using properties is typically recommended. - -If your type has more than one constructor, you must mark one of them using the -[`CommandLineConstructorAttribute`][] attribute. You don’t need to use this attribute if you have -only one constructor. - -If you don’t wish to define arguments using the constructor, simply use a constructor without any -parameters (or don’t define an explicit constructor). - -If you follow .Net coding conventions, property names will be PascalCase and parameter names will be -camelCase. If you use both to define arguments, and rely on automatically determined names, this -causes inconsistent naming for your arguments. You can fix this by specifying explicit names for -either type of argument, or by using a [name transformation](#name-transformation) to make all -automatic names consistent. - -### Nullable reference types - -One area where constructor parameters offer an advantage is when using non-nullable reference types. - -If you use a a property to define an argument whose type is a non-nullable reference type, the C# -compiler requires you to initialize it to a non-null value. - -```csharp -[CommandLineArgument(Position = 0, IsRequired = true)] -public string SomeArgument { get; set; } = string.Empty; -``` - -The compiler requires the initialization in this example, even though the argument is requires and -the initial value will therefore always be replaced by the [`CommandLineParser`][], unless you -instantiate the class manually without using [`CommandLineParser`][]. - -Constructor parameters provide a way to use a non-nullable reference type without requiring the -unnecessary initialization: - -```csharp -public MyArguments(string someArgument) -{ - SomeArgument = someArgument; -} - -public string SomeArgument { get; } -``` - -In this case, initialization is performed by the constructor, and (if using .Net 6.0 or later), -the [`CommandLineParser`][] class guarantees it will never pass a non-null value to the constructor -if the type is not nullable. +Method arguments allow all the same customizations as property-defined arguments, except that the +[`DefaultValue`][DefaultValue_1] will not be used. The method will never be invoked if the argument +is not explicitly specified by the user. -### CommandLineParser injection +## Applying parse options -If your constructor has a parameter whose type is [`CommandLineParser`][], this does not define an -argument. Instead, this property will be set to the [`CommandLineParser`][] instance that was used -to parse the arguments. This is useful if you want to access the [`CommandLineParser`][] instance -after parsing for whatever reason (for example, to see which alias was used to specify a particular -argument), but still want to use the static [`Parse()`][Parse()_1] method for automatic error -and usage help handling. +You can set parse options when you use the [`CommandLineParser`][] class using the [`ParseOptions`][] +class, but you can also set many common options on the arguments class directly using the +[`ParseOptionsAttribute`][] class. -Using [`CommandLineParser`][] injection can be used by itself, or combined with other parameters that -define arguments. +For example, the following disables the use of the `/` argument prefix on Windows, and always uses +only `-`. ```csharp -public MyArguments(CommandLineParser parser, string argument) +[GeneratedParser] +[ParseOptions(ArgumentNamesPrefixes = new[] { '-' })] +partial class Arguments { } ``` -## Long/short mode +### 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 using long/short mode, the name derived from the member or constructor parameter name, or the -explicit name set by the [`CommandLineArgumentAttribute`][] or [`ArgumentNameAttribute`][] attribute -is the long name. +A convenient [`IsPosix`][IsPosix_2] property is provided on either class, that sets all relevant options when +set to true. + +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. To set a short name, set [`CommandLineArgumentAttribute.ShortName`][] property. Alternatively, you can set the [`CommandLineArgumentAttribute.IsShort`][] property to true to use the first character -of the long name (after name transformation) as the short name. For constructor parameters, you use -the [`ArgumentNameAttribute.IsShort`][] and [`ArgumentNameAttribute.ShortName`][] properties for this -purpose. +of the long name (after name transformation) as the short name. -You can disable the long name using the [`CommandLineArgumentAttribute.IsLong`][] or -[`ArgumentNameAttribute.IsLong`][] property, in which case the argument will only have a short name. +You can disable the long name using the [`CommandLineArgumentAttribute.IsLong`][] property, in which +case the argument will only have a short name. ```csharp -[ParseOptions(Mode = ParsingMode.LongShort, - CaseSensitive = true, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionNameTransform = NameTransform.DashCase)] -class MyArguments +[GeneratedParser] +[ParseOptions(IsPosix = true)] +partial class MyArguments { - public MyArguments([ArgumentName(IsShort = true)] string fileName) - { - FileName = fileName; - } - - public string FileName { get; } + [CommandLineArgument(IsPositional = true, IsShort = true)] + public required string FileName { get; set } [CommandLineArgument(ShortName = 'F')] public int Foo { get; set;} @@ -487,7 +504,16 @@ class MyArguments } ``` -In this example, the `fileName` constructor parameter defines an argument with the long name +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 enabled. The `Bar` property defines an argument with the short name `-b`, but no long name. The @@ -496,8 +522,7 @@ names are all lower case due to the name transformation. ## Defining aliases An alias is an alternative name that can be used to specify a command line argument. Aliases can be -added to a command line argument by applying the [`AliasAttribute`][] to the property, method, or -constructor parameter that defines the argument. +added to a command line argument by applying the [`AliasAttribute`][] to the property or method. For example, the following code defines a switch argument that can be specified using either the name `-Verbose` or the alias `-v`: @@ -510,9 +535,28 @@ public bool Verbose { get; set; } To specify more than one alias for an argument, simply apply the [`AliasAttribute`][] multiple times. -When using [long/short mode](Arguments.md#longshort-mode), the [`AliasAttribute`][] specifies long name -aliases, and will be ignored if the argument doesn't have a long name. Use the [`ShortAliasAttribute`][] -to specify short aliases. These will be ignored if the argument doesn't have a short name. +When using [long/short mode](Arguments.md#longshort-mode), the [`AliasAttribute`][] specifies long +name aliases, and will be ignored if the argument doesn't have a long name. Use the +[`ShortAliasAttribute`][] to specify short aliases. These will be ignored if the argument doesn't +have a short name. + +## Automatic prefix aliases + +By default, Ookii.CommandLine will accept any prefix that uniquely identifies an argument by either +its name or one of its explicit aliases as an alias. For example, if you have an argument named +`-File`, it would be possible to specify it with `-F`, `-Fi`, and `-Fil`, as well as `-File`, +assuming none of those prefixes are ambiguous. + +In the above example using the `-Verbose` argument, `-v` would be ambiguous between `-Verbose` and +the [automatic `-Version` argument](#automatic-arguments), so it would not work as an alias without +explicitly specifying it. However, `-Verb` would work as an automatic prefix alias for `-Verbose`, +because it is not ambiguous. + +Automatic prefix aliases will not be shown in the [usage help](UsageHelp.md), so it can still be +useful to explicitly define an alias even if it's a prefix, if you wish to call more attention to it. + +If you do not want to use automatic prefix aliases, set the [`ParseOptionsAttribute.AutoPrefixAliases`][] +or [`ParseOptions.AutoPrefixAliases`][] property to false. ## Name transformation @@ -530,12 +574,13 @@ Value | Description **SnakeCase** | Member names are transformed to snake_case. This removes leading and trailing underscores, changes all characters to lower-case, and reduces consecutive underscores to a single underscore. An underscore is inserted before previously capitalized letters. | `SomeName`, `someName`, `_someName_` => some_name **DashCase** | Member names are transformed to dash-case. Similar to SnakeCase, but uses a dash instead of an underscore. | `SomeName`, `someName`, `_someName_` => some-name -Name transformations are set by using the [`ParseOptions.ArgumentNameTransform`][] property, or the [`ParseOptionsAttribute`][] which -can be applied to your arguments class. +Name transformations are set by using the [`ParseOptions.ArgumentNameTransform`][] property, or the [`ParseOptionsAttribute`][] +attribute. ```csharp +[GeneratedParser] [ParseOptions(ArgumentNameTransform = NameTransform.DashCase)] -class Arguments +partial class Arguments { [CommandLineArgument] public string? SomeArgument; @@ -548,24 +593,6 @@ class Arguments This defines two arguments named `-some-argument` and `-other-argument`, without the need to specify explicit names. -This can be useful if you combine constructor parameters and properties to define arguments. - -```csharp -[ParseOptions(ArgumentNameTransform = NameTransform.PascalCase)] -class Arguments -{ - public Arguments(string someArgument) - { - } - - [CommandLineArgument] - public int OtherArgument; -} -``` - -In this case the constructor-defined argument name will be `-SomeArgument`, consistent with the -property-defined argument `-OtherArgument`, without needing to use explicit names. - If you have an argument with an automatic short name when using [long/short mode](Arguments.md#longshort-mode), name transformation is applied to the name before the short name is determined, so the case of the short name will match the case of the first letter of the transformed long name. @@ -591,50 +618,58 @@ names. So with long/short mode and the dash-case transformation, you would have The names and aliases of the automatic arguments can be customized using the [`LocalizedStringProvider`][] class. -If your class defined an argument with the a name or alias matching the names or aliases of either +If your class defines an argument where the name or an alias matches the names or aliases of either of the automatic arguments, that argument will not be automatically added. In addition, you can -disable either automatic argument using the [`ParseOptions`][]. +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-3.1/html/T_Ookii_CommandLine_AliasAttribute.htm -[`ArgumentNameAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ArgumentNameAttribute.htm -[`ArgumentNameAttribute.IsLong`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ArgumentNameAttribute_IsLong.htm -[`ArgumentNameAttribute.IsShort`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ArgumentNameAttribute_IsShort.htm -[`ArgumentNameAttribute.ShortName`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ArgumentNameAttribute_ShortName.htm -[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm -[`CommandLineArgumentAttribute.IsRequired`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm -[`CommandLineArgumentAttribute.IsShort`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm -[`CommandLineArgumentAttribute.Position`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm -[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm -[`CommandLineArgumentAttribute.ValueDescription`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ValueDescription.htm -[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm -[`CommandLineConstructorAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineConstructorAttribute.htm -[`CommandLineParser.HelpRequested`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.htm +[`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 [`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 [`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 [`List`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1 -[`List`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1 -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`ParseOptions.ArgumentNameTransform`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptions.htm -[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm -[`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ShortAliasAttribute.htm -[`SortedDictionary`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.sorteddictionary-2 +[`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 +[`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 [`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 -[`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute -[`TypeConverterBase`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_TypeConverterBase_1.htm -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[CancelParsing_1]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[CommandLineParser.Parse()_2]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm -[DefaultValue_1]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm - -[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`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 diff --git a/docs/Migrating.md b/docs/Migrating.md index 3235eab7..d075041b 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -1,10 +1,11 @@ -# Migrating from Ookii.CommandLine 2.x +# Migrating from Ookii.CommandLine 2.x / 3.x -Ookii.CommandLine 3.0 and later have a number of breaking changes from version 2.4 and earlier +Ookii.CommandLine 4.0 and later have a number of breaking changes from version 3.x and earlier versions. This article explains what you need to know to migrate your code to the new version. Although there are quite a few changes, it's likely your application will not require many -modifications unless you used subcommands or heavily customized the usage help format. +modifications unless you used subcommands, heavily customized the usage help format, or used +custom argument value conversion. ## .Net Framework support @@ -12,7 +13,72 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ 4.6.1 and later using the .Net Standard 2.0 assembly. If you need to support an older version of .Net, please continue to use [version 2.4](https://github.com/SvenGroot/ookii.commandline/releases/tag/v2.4). -## Breaking API changes +## Breaking API changes from version 3.0 + +- It's strongly recommended to apply the [`GeneratedParserAttribute`][] to your arguments classes + unless you cannot meet the requirements for [source generation](SourceGeneration.md). +- The `CommandLineArgumentAttribute.ValueDescription` property has been replaced by the + [`ValueDescriptionAttribute`][] attribute. This new attribute is not sealed, enabling derived + attributes e.g. to load a value description from a localized resource. +- Converting argument values from a string to their final type is no longer done using the + [`TypeConverter`][] class, but instead using a custom [`ArgumentConverter`][] class. Custom + converters must be specified using the [`ArgumentConverterAttribute`][] instead of the + [`TypeConverterAttribute`][]. + - If you have existing conversions that depend on a [`TypeConverter`][], use the + [`WrappedTypeConverter`][] and [`WrappedDefaultTypeConverter`][] as a convenient way to + keep using that conversion. + - The [`KeyValuePairConverter`][] class has moved into the + [`Ookii.CommandLine.Conversion`][] namespace. + - The [`KeyValueSeparatorAttribute`][] has moved into the [`Ookii.CommandLine.Conversion`][] + namespace. + - The `KeyTypeConverterAttribute` and `ValueTypeConverterAttribute` were renamed to + [`KeyConverterAttribute`][] and [`ValueConverterAttribute`][] respectively +- Constructor parameters can no longer be used to define command line arguments. Instead, all + arguments must be defined using properties. +- `ParseOptions.ArgumentNameComparer` and `CommandOptions.CommandNameComparer` have been replaced by + [`ArgumentNameComparison`][ArgumentNameComparison_1] and [`CommandNameComparison`][] respectively, + both now taking a [`StringComparison`][] value instead of an [`IComparer`][]. +- Overloads of the [`CommandLineParser.Parse()`][CommandLineParser.Parse()_2], [`CommandLineParser.ParseWithErrorHandling()`][], + [`CommandLineParser.Parse()`][], [`CommandLineParser.ParseWithErrorHandling()`][], + [`CommandManager.CreateCommand()`][] and [`CommandManager.RunCommand()`][] methods that took an index have + been replaced by overloads that take a [`ReadOnlyMemory`][]. +- The [`CommandInfo`][] type is now a class instead of a structure. +- The [`ICommandWithCustomParsing.Parse()`][] method signature has changed to use a + [`ReadOnlyMemory`][] structure for the arguments and to receive a reference to the calling + [`CommandManager`][] instance. +- The [`CommandLineArgumentAttribute.CancelParsing`][] property now takes a [`CancelMode`][] + enumeration rather than a boolean. +- The [`ArgumentParsedEventArgs`][] class was changed to use the [`CancelMode`][] enumeration. +- Canceling parsing using the [`ArgumentParsed`][] event no longer automatically sets the [`HelpRequested`][] + property; instead, you must set it manually in the event handler if desired. +- The `ParseOptionsAttribute.NameValueSeparator` property was replaced with + [`ParseOptionsAttribute.NameValueSeparators`][]. +- The `ParseOptions.NameValueSeparator` property was replaced with + [`ParseOptions.NameValueSeparators`][]. +- Properties that previously returned a [`ReadOnlyCollection`][] now return an + [`ImmutableArray`][]. +- The `CommandLineArgument.MultiValueSeparator` and `CommandLineArgument.AllowMultiValueWhiteSpaceSeparator` + properties have been moved into the [`CommandLineArgument.MultiValueInfo`][] property. +- The `CommandLineArgument.AllowsDuplicateDictionaryKeys` and `CommandLineArgument.KeyValueSeparator` + properties have been moved into the [`CommandLineArgument.DictionaryInfo`][] property. +- The `CommandLineArgument.IsDictionary` and `CommandLineArgument.IsMultiValue` properties have been + removed; instead, check [`CommandLineArgument.DictionaryInfo`][] or [`CommandLineArgument.MultiValueInfo`][] + for null values, or use the [`CommandLineArgument.Kind`][] property. +- [`TextFormat`][] is now a structure with strongly-typed values for VT sequences, and that structure is + used by the [`UsageWriter`][] class for the various color formatting options. + +## Breaking behavior changes from version 3.0 + +- By default, both `:` and `=` are accepted as argument name/value separators. +- The default value of [`ParseOptions.ShowUsageOnError`][] has changed to [`UsageHelpRequest.SyntaxOnly`][]. +- [Automatic prefix aliases](DefiningArguments.md#automatic-prefix-aliases) are enabled by default + for both argument names and [command names](Subcommands.md#command-aliases). +- The [`CommandManager`][], when using an assembly that is not the calling assembly, will only use + public command classes, where before it would also use internal ones. This is to better respect + access modifiers, and to make sure generated and reflection-based command managers behave the + same. + +## Breaking API changes from version 2.4 - It's strongly recommended to switch to the static [`CommandLineParser.Parse()`][] method, if you were not already using it from version 2.4. @@ -55,7 +121,7 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - The [`CommandLineArgument.ElementType`][] property now returns the underlying type for arguments using the [`Nullable`][] type. -## Breaking behavior changes +## Breaking behavior changes from version 2.4 - Argument type conversion now defaults to [`CultureInfo.InvariantCulture`][], instead of [`CurrentCulture`][]. This change was made to ensure a consistent parsing experience regardless of the @@ -71,29 +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. -[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm -[`CommandAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm -[`CommandLineArgument.ElementType`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgument_ElementType.htm -[`CommandLineParser.HelpRequested`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser.WriteUsage()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_WriteUsage.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`CommandOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandOptions.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 +[`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 [`CultureInfo.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.invariantculture [`CurrentCulture`]: https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.currentculture -[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm -[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[`ICommand`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_ICommand.htm -[`ICommandWithCustomParsing.Parse()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse.htm -[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`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 +[`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 [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 -[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-3.1/html/N_Ookii_CommandLine_Commands.htm -[`ParseOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptions.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm -[CommandLineParser.Parse()_2]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm -[Parse()_5]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[Parse()_6]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_CommandLineParser_Parse.htm +[`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 +[`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 +[`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 diff --git a/docs/Ookii.CommandLine.shfbproj b/docs/Ookii.CommandLine.shfbproj index 45fd5118..e4b42177 100644 --- a/docs/Ookii.CommandLine.shfbproj +++ b/docs/Ookii.CommandLine.shfbproj @@ -31,15 +31,21 @@ Provides functionality for creating applications with multiple subcommands, each with their own arguments. </para> <para> - Provides helpers for supporting virtual terminal sequences and color output. + Provides helpers for using virtual terminal sequences and color output on the console. </para> <para> Provides attributes used to validate the value of arguments, and the relation between arguments. +</para> + <para> + Provides functionality for converting argument strings from the command line to the actual type of an argument. +</para> + <para> + Provides types to support source generation. Types in this namespace should not be used directly in your code. </para> https://github.com/SvenGroot/Ookii.CommandLine Copyright &#169%3b Sven Groot %28Ookii.org%29 - Ookii.CommandLine 3.1 documentation + Ookii.CommandLine 4.0 documentation MemberName Default2022 C#, Visual Basic, Visual Basic Usage, Managed C++ @@ -84,6 +90,8 @@ &lt%3bpara&gt%3b Functionality for creating applications that support multiple subcommands, where each command has its own arguments, is provided by the &lt%3bsee cref=&quot%3bT:Ookii.CommandLine.Commands.CommandManager&quot%3b/&gt%3b class. &lt%3b/para&gt%3b + v4.8 + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs new file mode 100644 index 00000000..3611f4d9 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -0,0 +1,983 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Diagnostics; + +namespace Ookii.CommandLine.Generator; + +internal class ParserGenerator +{ + private enum ReturnType + { + Void, + Boolean, + CancelMode + } + + private struct MethodArgumentInfo + { + public ITypeSymbol ArgumentType { get; set; } + public bool HasValueParameter { get; set; } + public bool HasParserParameter { get; set; } + public ReturnType ReturnType { get; set; } + } + + private struct PositionalArgumentInfo + { + public int Position { get; set; } + public ISymbol Member { get; set; } + public bool IsRequired { get; set; } + public bool IsMultiValue { get; set; } + } + + private readonly TypeHelper _typeHelper; + private readonly Compilation _compilation; + private readonly SourceProductionContext _context; + private readonly INamedTypeSymbol _argumentsClass; + private readonly SourceBuilder _builder; + private readonly ConverterGenerator _converterGenerator; + private readonly CommandGenerator _commandGenerator; + private readonly LanguageVersion _languageVersion; + private bool _hasImplicitPositions; + private int _nextImplicitPosition; + private Dictionary? _positions; + private List? _positionalArguments; + + public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, + ConverterGenerator converterGenerator, CommandGenerator commandGenerator, LanguageVersion languageVersion) + { + _typeHelper = typeHelper; + _compilation = typeHelper.Compilation; + _context = context; + _argumentsClass = argumentsClass; + _builder = new(argumentsClass.ContainingNamespace); + _converterGenerator = converterGenerator; + _commandGenerator = commandGenerator; + _languageVersion = languageVersion; + } + + public static string? Generate(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, + ConverterGenerator converterGenerator, CommandGenerator commandGenerator, LanguageVersion languageVersion) + { + var generator = new ParserGenerator(context, argumentsClass, typeHelper, converterGenerator, commandGenerator, + languageVersion); + + return generator.Generate(); + } + + public string? Generate() + { + // Find the attributes that can apply to an arguments class. + // This code also finds attributes that inherit from those attribute. By instantiating the + // possibly derived attribute classes, we can support for example a class that derives from + // DescriptionAttribute that gets the description from a resource. + var attributes = new ArgumentsClassAttributes(_argumentsClass, _typeHelper); + + var isCommand = false; + if (attributes.Command != null) + { + if (_argumentsClass.ImplementsInterface(_typeHelper.ICommandWithCustomParsing)) + { + _context.ReportDiagnostic(Diagnostics.GeneratedCustomParsingCommand(_argumentsClass)); + return null; + } + else if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) + { + isCommand = true; + _commandGenerator.AddGeneratedCommand(_argumentsClass, attributes); + } + else + { + _context.ReportDiagnostic(Diagnostics.CommandAttributeWithoutInterface(_argumentsClass)); + } + } + else if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) + { + // Although this is a common pattern for base classes, it makes no sense to apply the + // GeneratedParserAttribute to a base class. + _context.ReportDiagnostic(Diagnostics.CommandInterfaceWithoutAttribute(_argumentsClass)); + } + + // Don't generate the parse methods for commands unless explicitly asked for. + var generateParseMethods = !isCommand; + foreach (var arg in attributes.GeneratedParser!.NamedArguments) + { + if (arg.Key == "GenerateParseMethods") + { + generateParseMethods = (bool)arg.Value.Value!; + break; + } + } + + _builder.AppendLine($"partial class {_argumentsClass.Name}"); + // Static interface methods require not just .Net 7 but also C# 11. + // There is no defined constant for C# 11 because the generator is built for .Net 6.0. + if (_typeHelper.IParser != null && _languageVersion >= (LanguageVersion)1100) + { + if (generateParseMethods) + { + _builder.AppendLine($" : Ookii.CommandLine.IParser<{_argumentsClass.Name}>"); + } + else + { + _builder.AppendLine($" : Ookii.CommandLine.IParserProvider<{_argumentsClass.Name}>"); + } + } + + _builder.OpenBlock(); + if (!GenerateProvider(attributes, isCommand)) + { + return null; + } + + if (isCommand) + { + if (attributes.Description == null) + { + var commandInfo = new CommandAttributeInfo(attributes.Command!); + if (!commandInfo.IsHidden) + { + _context.ReportDiagnostic(Diagnostics.CommandWithoutDescription(_argumentsClass)); + } + } + + if (attributes.ApplicationFriendlyName != null) + { + _context.ReportDiagnostic(Diagnostics.IgnoredFriendlyNameAttribute(_argumentsClass, attributes.ApplicationFriendlyName)); + } + } + else + { + if (attributes.ParentCommand != null) + { + _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForNonCommand(_argumentsClass, attributes.ParentCommand)); + } + } + + _builder.AppendLine(); + _builder.AppendLine("/// "); + _builder.AppendLine("/// Creates a instance using the specified options."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// "); + _builder.AppendLine("/// The options that control parsing behavior, or to use the"); + _builder.AppendLine("/// default options."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// "); + _builder.AppendLine($"/// An instance of the class for the class."); + _builder.AppendLine("/// "); + _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); + + if (generateParseMethods) + { + // We cannot rely on default interface implementations, because that makes the methods + // uncallable without a generic type argument. + _builder.AppendLine("/// "); + _builder.AppendLine("/// Parses the arguments returned by the "); + _builder.AppendLine("/// method, handling errors and showing usage help as required."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// "); + _builder.AppendLine("/// The options that control parsing behavior and usage help formatting. If"); + _builder.AppendLine("/// , the default options are used."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// "); + _builder.AppendLine($"/// An instance of the class, or if an"); + _builder.AppendLine("/// error occurred or argument parsing was canceled."); + _builder.AppendLine("/// "); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); + _builder.AppendLine(); + _builder.AppendLine("/// "); + _builder.AppendLine("/// Parses the specified command line arguments, handling errors and showing usage help as required."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// The command line arguments."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// The options that control parsing behavior and usage help formatting. If"); + _builder.AppendLine("/// , the default options are used."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// "); + _builder.AppendLine($"/// An instance of the class, or if an"); + _builder.AppendLine("/// error occurred or argument parsing was canceled."); + _builder.AppendLine("/// "); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); + _builder.AppendLine(); + _builder.AppendLine("/// "); + _builder.AppendLine("/// Parses the specified command line arguments, handling errors and showing usage help as required."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// The command line arguments."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// The options that control parsing behavior and usage help formatting. If"); + _builder.AppendLine("/// , the default options are used."); + _builder.AppendLine("/// "); + _builder.AppendLine("/// "); + _builder.AppendLine($"/// An instance of the class, or if an"); + _builder.AppendLine("/// error occurred or argument parsing was canceled."); + _builder.AppendLine("/// "); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(System.ReadOnlyMemory args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); + _builder.CloseBlock(); // class + } + + return _builder.GetSource(); + } + + private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isCommand) + { + _builder.AppendLine("private class OokiiCommandLineArgumentProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); + _builder.OpenBlock(); + _builder.AppendLine("public OokiiCommandLineArgumentProvider()"); + _builder.IncreaseIndent(); + _builder.AppendLine(": base("); + _builder.IncreaseIndent(); + _builder.AppendArgument($"typeof({_argumentsClass.Name})"); + AppendOptionalAttribute(attributes.ParseOptions, "options"); + AppendOptionalAttribute(attributes.ClassValidators, "validators", "Ookii.CommandLine.Validation.ClassValidationAttribute"); + AppendOptionalAttribute(attributes.ApplicationFriendlyName, "friendlyName"); + AppendOptionalAttribute(attributes.Description, "description"); + _builder.CloseArgumentList(false); + _builder.DecreaseIndent(); + _builder.AppendLine("{}"); + _builder.AppendLine(); + _builder.AppendLine($"public override bool IsCommand => {isCommand.ToCSharpString()};"); + _builder.AppendLine(); + _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); + _builder.OpenBlock(); + + List<(string, string, string)>? requiredProperties = null; + var hasError = false; + + // Build a stack with the base types because we have to consider them first to get the + // correct order for auto positional arguments. + var argumentTypes = new Stack(); + for (var current = _argumentsClass; + current != null && current.SpecialType == SpecialType.None; + current = current.BaseType) + { + argumentTypes.Push(current); + } + + foreach (var type in argumentTypes) + { + foreach (var member in type.GetMembers()) + { + if (!GenerateArgument(member, ref requiredProperties)) + { + hasError = true; + } + } + } + + if (!VerifyPositionalArgumentRules()) + { + return false; + } + + // Makes sure the function compiles if there are no arguments. + _builder.AppendLine("yield break;"); + _builder.CloseBlock(); // GetArguments() + _builder.AppendLine(); + _builder.AppendLine("public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser, object?[]? requiredPropertyValues)"); + _builder.OpenBlock(); + if (_argumentsClass.FindConstructor(_typeHelper.CommandLineParser) != null) + { + _builder.Append($"return new {_argumentsClass.Name}(parser)"); + } + else + { + _builder.Append($"return new {_argumentsClass.Name}()"); + } + + if (requiredProperties == null) + { + _builder.AppendLine(";"); + } + else + { + _builder.AppendLine(); + _builder.OpenBlock(); + for (int i = 0; i < requiredProperties.Count; ++i) + { + var property = requiredProperties[i]; + _builder.Append($"{property.Item1} = ({property.Item2})requiredPropertyValues![{i}]{property.Item3}"); + if (i < requiredProperties.Count - 1) + { + _builder.Append(","); + } + + _builder.AppendLine(); + } + + _builder.DecreaseIndent(); + _builder.AppendLine("};"); + } + + _builder.CloseBlock(); // CreateInstance() + _builder.CloseBlock(); // OokiiCommandLineArgumentProvider class + return !hasError; + } + + private bool GenerateArgument(ISymbol member, ref List<(string, string, string)>? requiredProperties) + { + // This shouldn't happen because of attribute targets, but check anyway. + if (member.Kind is not (SymbolKind.Method or SymbolKind.Property)) + { + return true; + } + + var attributes = new ArgumentAttributes(member, _typeHelper, _context); + + // Check if it is an argument. + if (attributes.CommandLineArgument == null) + { + return true; + } + + var argumentInfo = new CommandLineArgumentAttributeInfo(attributes.CommandLineArgument); + if (!argumentInfo.IsLong && !argumentInfo.IsShort) + { + _context.ReportDiagnostic(Diagnostics.NoLongOrShortName(member, attributes.CommandLineArgument)); + return false; + } + + ITypeSymbol originalArgumentType; + MethodArgumentInfo? methodInfo = null; + var property = member as IPropertySymbol; + if (property != null) + { + if (property.DeclaredAccessibility != Accessibility.Public || property.IsStatic) + { + _context.ReportDiagnostic(Diagnostics.NonPublicInstanceProperty(property)); + return true; + } + + originalArgumentType = property.Type; + } + else if (member is IMethodSymbol method) + { + if (method.DeclaredAccessibility != Accessibility.Public || !method.IsStatic) + { + _context.ReportDiagnostic(Diagnostics.NonPublicStaticMethod(method)); + return true; + } + + methodInfo = DetermineMethodArgumentInfo(method); + if (methodInfo is not MethodArgumentInfo methodInfoValue) + { + _context.ReportDiagnostic(Diagnostics.InvalidMethodSignature(method)); + return false; + } + + originalArgumentType = methodInfoValue.ArgumentType; + } + else + { + // How did we get here? Already checked above. + return true; + } + + var argumentType = originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var notNullAnnotation = string.Empty; + var allowsNull = originalArgumentType.AllowsNull(); + if (allowsNull) + { + // Needed in case the original definition was in a context without NRT support. + originalArgumentType = originalArgumentType.WithNullableAnnotation(NullableAnnotation.Annotated); + } + else + { + notNullAnnotation = "!"; + } + + var elementTypeWithNullable = argumentType; + var namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; + ITypeSymbol? keyType = null; + ITypeSymbol? valueType = null; + if (attributes.KeyValueSeparator != null) + { + _builder.AppendLine($"var keyValueSeparatorAttribute{member.Name} = {attributes.KeyValueSeparator.CreateInstantiation()};"); + } + + var isMultiValue = false; + var isDictionary = false; + var isRequired = argumentInfo.IsRequired; + var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; + string? converter = null; + if (property != null) + { + var multiValueType = DetermineMultiValueType(property, argumentType); + if (multiValueType is not var (collectionType, dictionaryType, multiValueElementType)) + { + return false; + } + + if (dictionaryType != null) + { + Debug.Assert(multiValueElementType != null); + kind = "Ookii.CommandLine.ArgumentKind.Dictionary"; + isMultiValue = true; + isDictionary = true; + elementTypeWithNullable = multiValueElementType!; + // KeyValuePair is guaranteed a named type. + namedElementTypeWithNullable = (INamedTypeSymbol)elementTypeWithNullable; + keyType = namedElementTypeWithNullable.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var rawValueType = namedElementTypeWithNullable.TypeArguments[1]; + allowsNull = rawValueType.AllowsNull(); + valueType = rawValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + if (attributes.Converter == null) + { + var keyConverter = DetermineConverter(member, keyType.GetUnderlyingType(), attributes.KeyConverter, keyType.IsNullableValueType()); + if (keyConverter == null) + { + _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); + return false; + } + + var valueConverter = DetermineConverter(member, valueType.GetUnderlyingType(), attributes.ValueConverter, valueType.IsNullableValueType()); + if (valueConverter == null) + { + _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); + return false; + } + + var separator = attributes.KeyValueSeparator == null + ? "null" + : $"keyValueSeparatorAttribute{member.Name}.Separator"; + + converter = $"new Ookii.CommandLine.Conversion.KeyValuePairConverter<{keyType.ToQualifiedName()}, {rawValueType.ToQualifiedName()}>({keyConverter}, {valueConverter}, {separator}, {allowsNull.ToCSharpString()})"; + } + } + else if (collectionType != null) + { + Debug.Assert(multiValueElementType != null); + kind = "Ookii.CommandLine.ArgumentKind.MultiValue"; + isMultiValue = true; + allowsNull = multiValueElementType!.AllowsNull(); + elementTypeWithNullable = multiValueElementType!.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; + } + + if (property.SetMethod != null && property.SetMethod.IsInitOnly && !property.IsRequired) + { + _context.ReportDiagnostic(Diagnostics.NonRequiredInitOnlyProperty(property)); + return false; + } + + if (property.IsRequired) + { + isRequired = true; + requiredProperties ??= new(); + requiredProperties.Add((member.Name, property.Type.ToQualifiedName(), notNullAnnotation)); + } + } + else + { + kind = "Ookii.CommandLine.ArgumentKind.Method"; + } + + var elementType = namedElementTypeWithNullable?.GetUnderlyingType() ?? elementTypeWithNullable; + converter ??= DetermineConverter(member, elementType, attributes.Converter, elementTypeWithNullable.IsNullableValueType()); + if (converter == null) + { + _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); + return false; + } + + // The leading commas are not a formatting I like but it does make things easier here. + _builder.AppendLine($"yield return Ookii.CommandLine.Support.GeneratedArgument.Create("); + _builder.IncreaseIndent(); + _builder.AppendArgument("parser"); + _builder.AppendArgument($"argumentType: typeof({argumentType.ToQualifiedName()})"); + _builder.AppendArgument($"elementTypeWithNullable: typeof({elementTypeWithNullable.ToQualifiedName()})"); + _builder.AppendArgument($"elementType: typeof({elementType.ToQualifiedName()})"); + _builder.AppendArgument($"memberName: \"{member.Name}\""); + _builder.AppendArgument($"kind: {kind}"); + _builder.AppendArgument($"attribute: {attributes.CommandLineArgument.CreateInstantiation()}"); + _builder.AppendArgument($"converter: {converter}"); + _builder.AppendArgument($"allowsNull: {(allowsNull.ToCSharpString())}"); + var valueDescriptionFormat = new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters); + if (keyType != null) + { + _builder.AppendArgument($"keyType: typeof({keyType.ToQualifiedName()})"); + _builder.AppendArgument($"defaultKeyDescription: \"{keyType.ToDisplayString(valueDescriptionFormat)}\""); + } + + if (valueType != null) + { + _builder.AppendArgument($"valueType: typeof({valueType.ToQualifiedName()})"); + _builder.AppendArgument($"defaultValueDescription: \"{valueType.ToDisplayString(valueDescriptionFormat)}\""); + } + else + { + _builder.AppendArgument($"defaultValueDescription: \"{elementType.ToDisplayString(valueDescriptionFormat)}\""); + } + + AppendOptionalAttribute(attributes.MultiValueSeparator, "multiValueSeparatorAttribute"); + AppendOptionalAttribute(attributes.Description, "descriptionAttribute"); + AppendOptionalAttribute(attributes.ValueDescription, "valueDescriptionAttribute"); + if (attributes.AllowDuplicateDictionaryKeys != null) + { + _builder.AppendArgument("allowDuplicateDictionaryKeys: true"); + } + + if (attributes.KeyValueSeparator != null) + { + _builder.AppendArgument($"keyValueSeparatorAttribute: keyValueSeparatorAttribute{member.Name}"); + } + + AppendOptionalAttribute(attributes.Aliases, "aliasAttributes", "Ookii.CommandLine.AliasAttribute"); + AppendOptionalAttribute(attributes.ShortAliases, "shortAliasAttributes", "Ookii.CommandLine.ShortAliasAttribute"); + AppendOptionalAttribute(attributes.Validators, "validationAttributes", "Ookii.CommandLine.Validation.ArgumentValidationAttribute"); + if (property != null) + { + if (property.SetMethod != null && property.SetMethod.DeclaredAccessibility == Accessibility.Public && !property.SetMethod.IsInitOnly) + { + _builder.AppendArgument($"setProperty: (target, value) => (({_argumentsClass.ToQualifiedName()})target).{member.Name} = ({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}"); + } + + _builder.AppendArgument($"getProperty: (target) => (({_argumentsClass.ToQualifiedName()})target).{member.Name}"); + _builder.AppendArgument($"requiredProperty: {property.IsRequired.ToCSharpString()}"); + if (argumentInfo.DefaultValue != null) + { + if (isMultiValue) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithMultiValue(member)); + } + else if (property.IsRequired || argumentInfo.IsRequired) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithRequired(member)); + } + } + + if (argumentInfo.HasIsRequired && property.IsRequired) + { + _context.ReportDiagnostic(Diagnostics.IsRequiredWithRequiredProperty(member)); + } + + // Check if we should use the initializer for a default value. + if (!isMultiValue && !property.IsRequired && !argumentInfo.IsRequired && argumentInfo.DefaultValue == null && argumentInfo.IncludeDefaultInUsageHelp) + { + var alternateDefaultValue = GetInitializerValue(property); + if (alternateDefaultValue != null) + { + _builder.AppendArgument($"alternateDefaultValue: {alternateDefaultValue}"); + } + } + } + + if (methodInfo is MethodArgumentInfo info) + { + string arguments = string.Empty; + if (info.HasValueParameter) + { + if (info.HasParserParameter) + { + arguments = $"({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}, parser"; + } + else + { + arguments = $"({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}"; + } + } + else if (info.HasParserParameter) + { + arguments = "parser"; + } + + var methodCall = info.ReturnType switch + { + ReturnType.CancelMode => $"callMethod: (value, parser) => {_argumentsClass.ToQualifiedName()}.{member.Name}({arguments})", + ReturnType.Boolean => $"callMethod: (value, parser) => {_argumentsClass.ToQualifiedName()}.{member.Name}({arguments}) ? Ookii.CommandLine.CancelMode.None : Ookii.CommandLine.CancelMode.Abort", + _ => $"callMethod: (value, parser) => {{ {_argumentsClass.ToQualifiedName()}.{member.Name}({arguments}); return Ookii.CommandLine.CancelMode.None; }}" + }; + + _builder.AppendArgument(methodCall); + if (argumentInfo.DefaultValue != null) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithMethod(member)); + } + } + + if (argumentInfo.Position is int position) + { + if (_hasImplicitPositions) + { + _context.ReportDiagnostic(Diagnostics.MixedImplicitExplicitPositions(_argumentsClass)); + return false; + } + + _positions ??= new(); + if (_positions.TryGetValue(position, out string name)) + { + _context.ReportDiagnostic(Diagnostics.DuplicatePosition(member, name)); + } + else + { + _positions.Add(position, member.Name); + } + + _positionalArguments ??= new(); + _positionalArguments.Add(new PositionalArgumentInfo() + { + Member = member, + Position = position, + IsRequired = isRequired, + IsMultiValue = isMultiValue + }); + } + else if (argumentInfo.IsPositional) + { + if (_positions != null) + { + _context.ReportDiagnostic(Diagnostics.MixedImplicitExplicitPositions(_argumentsClass)); + return false; + } + + _hasImplicitPositions = true; + _builder.AppendArgument($"position: {_nextImplicitPosition}"); + ++_nextImplicitPosition; + } + + _builder.CloseArgumentList(); + _builder.AppendLine(); + + // Can't check if long/short name is actually used, or whether the '-' prefix is used for + // either style, since ParseOptions might change that. So, just warn either way. + if (!string.IsNullOrEmpty(argumentInfo.ArgumentName) && char.IsDigit(argumentInfo.ArgumentName![0])) + { + _context.ReportDiagnostic(Diagnostics.ArgumentStartsWithNumber(member, argumentInfo.ArgumentName)); + } + else if (char.IsDigit(argumentInfo.ShortName)) + { + _context.ReportDiagnostic(Diagnostics.ArgumentStartsWithNumber(member, argumentInfo.ShortName.ToString())); + } + + + if (!argumentInfo.IsShort && attributes.ShortAliases != null) + { + _context.ReportDiagnostic(Diagnostics.ShortAliasWithoutShortName(attributes.ShortAliases.First(), member)); + } + + if (!argumentInfo.IsLong && attributes.Aliases != null) + { + _context.ReportDiagnostic(Diagnostics.AliasWithoutLongName(attributes.Aliases.First(), member)); + } + + bool isHidden = false; + if (argumentInfo.IsHidden) + { + if (argumentInfo.IsPositional || argumentInfo.IsRequired || (property?.IsRequired ?? false)) + { + _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositionalOrRequired(member)); + } + else + { + isHidden = true; + } + } + + if (!isHidden && attributes.Description == null) + { + _context.ReportDiagnostic(Diagnostics.ArgumentWithoutDescription(member)); + } + + CheckIgnoredDictionaryAttribute(member, isDictionary, attributes.Converter, attributes.KeyConverter); + CheckIgnoredDictionaryAttribute(member, isDictionary, attributes.Converter, attributes.ValueConverter); + CheckIgnoredDictionaryAttribute(member, isDictionary, attributes.Converter, attributes.KeyValueSeparator); + if (!isMultiValue && attributes.MultiValueSeparator != null) + { + _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForNonMultiValue(member, attributes.MultiValueSeparator)); + } + + if (!isDictionary && attributes.AllowDuplicateDictionaryKeys != null) + { + _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForNonDictionary(member, attributes.AllowDuplicateDictionaryKeys)); + } + + if (argumentInfo.ShortName != '\0' && argumentInfo.ExplicitIsShort == false) + { + _context.ReportDiagnostic(Diagnostics.IsShortIgnored(member, attributes.CommandLineArgument)); + } + + return true; + } + + private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) + { + if (argumentType is INamedTypeSymbol namedType) + { + // If the type is Dictionary it doesn't matter if the property is + // read-only or not. + if (namedType.IsGenericType && namedType.ConstructedFrom.SymbolEquals(_typeHelper.Dictionary)) + { + var keyValuePair = _compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!; + var elementType = keyValuePair.Construct(namedType.TypeArguments, namedType.TypeArgumentNullableAnnotations); + return (null, namedType, elementType); + } + } + + if (argumentType is IArrayTypeSymbol arrayType) + { + if (arrayType.Rank != 1) + { + _context.ReportDiagnostic(Diagnostics.InvalidArrayRank(property)); + return null; + } + + if (property.SetMethod?.DeclaredAccessibility != Accessibility.Public) + { + _context.ReportDiagnostic(Diagnostics.PropertyIsReadOnly(property)); + return null; + } + + var elementType = arrayType.ElementType; + return (argumentType, null, elementType); + } + + // The interface approach requires a read-only property. If it's read-write, treat it + // like a non-multi-value argument. + if (property.SetMethod?.DeclaredAccessibility == Accessibility.Public) + { + return (null, null, null); + } + + var dictionaryType = argumentType.FindGenericInterface(_typeHelper.IDictionary); + if (dictionaryType != null) + { + var keyValuePair = _compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!; + var elementType = keyValuePair.Construct(dictionaryType.TypeArguments, dictionaryType.TypeArgumentNullableAnnotations); + return (null, dictionaryType, elementType); + } + + var collectionType = argumentType.FindGenericInterface(_typeHelper.ICollection); + if (collectionType != null) + { + var elementType = collectionType.TypeArguments[0]; + return (collectionType, null, elementType); + } + + // This is a read-only property with an unsupported type. + _context.ReportDiagnostic(Diagnostics.PropertyIsReadOnly(property)); + return null; + } + + public string? DetermineConverter(ISymbol member, ITypeSymbol elementType, AttributeData? converterAttribute, bool isNullableValueType) + { + var converter = DetermineElementConverter(member, elementType, converterAttribute); + if (converter != null && isNullableValueType) + { + converter = $"new Ookii.CommandLine.Conversion.NullableConverter({converter})"; + } + + return converter; + } + + public string? DetermineElementConverter(ISymbol member, ITypeSymbol elementType, AttributeData? converterAttribute) + { + if (converterAttribute != null) + { + var argument = converterAttribute.ConstructorArguments[0]; + if (argument.Kind != TypedConstantKind.Type) + { + _context.ReportDiagnostic(Diagnostics.ArgumentConverterStringNotSupported(converterAttribute, member)); + return null; + } + + var converterType = (INamedTypeSymbol)argument.Value!; + return $"new {converterType.ToQualifiedName()}()"; + } + + if (elementType.SpecialType == SpecialType.System_String) + { + return "Ookii.CommandLine.Conversion.StringConverter.Instance"; + } + else if (elementType.SpecialType == SpecialType.System_Boolean) + { + return "Ookii.CommandLine.Conversion.BooleanConverter.Instance"; + } + + if (elementType.TypeKind == TypeKind.Enum) + { + return $"new Ookii.CommandLine.Conversion.EnumConverter(typeof({elementType.ToQualifiedName()}))"; + } + + if (elementType.ImplementsInterface(_typeHelper.ISpanParsable?.Construct(elementType))) + { + return $"new Ookii.CommandLine.Conversion.SpanParsableConverter<{elementType.ToQualifiedName()}>()"; + } + + if (elementType.ImplementsInterface(_typeHelper.IParsable?.Construct(elementType))) + { + return $"new Ookii.CommandLine.Conversion.ParsableConverter<{elementType.ToQualifiedName()}>()"; + } + + return _converterGenerator.GetConverter(elementType); + } + + private MethodArgumentInfo? DetermineMethodArgumentInfo(IMethodSymbol method) + { + var parameters = method.Parameters; + if (!method.IsStatic || parameters.Length > 2) + { + return null; + } + + var info = new MethodArgumentInfo(); + if (method.ReturnType.SymbolEquals(_typeHelper.CancelMode)) + { + info.ReturnType = ReturnType.CancelMode; + } + else if (method.ReturnType.SpecialType == SpecialType.System_Boolean) + { + info.ReturnType = ReturnType.Boolean; + } + else if (method.ReturnType.SpecialType != SpecialType.System_Void) + { + return null; + } + + if (parameters.Length == 2) + { + info.ArgumentType = parameters[0].Type; + if (!parameters[1].Type.SymbolEquals(_typeHelper.CommandLineParser)) + { + return null; + } + + info.HasValueParameter = true; + info.HasParserParameter = true; + } + else if (parameters.Length == 1) + { + if (parameters[0].Type.SymbolEquals(_typeHelper.CommandLineParser)) + { + info.ArgumentType = _typeHelper.Boolean!; + info.HasParserParameter = true; + } + else + { + info.ArgumentType = parameters[0].Type; + info.HasValueParameter = true; + } + } + else + { + info.ArgumentType = _typeHelper.Boolean!; + } + + return info; + } + + private void AppendOptionalAttribute(AttributeData? attribute, string name) + { + if (attribute != null) + { + _builder.AppendArgument($"{name}: {attribute.CreateInstantiation()}"); + } + } + + private void AppendOptionalAttribute(List? attributes, string name, string typeName) + { + if (attributes != null) + { + _builder.AppendArgument($"{name}: new {typeName}[] {{ {string.Join(", ", attributes.Select(a => a.CreateInstantiation()))} }}"); + } + } + + private bool VerifyPositionalArgumentRules() + { + if (_positionalArguments == null) + { + return true; + } + + // This mirrors the logic in CommandLineParser.VerifyPositionalArgumentRules. + _positionalArguments.Sort((x, y) => x.Position.CompareTo(y.Position)); + string? multiValueArgument = null; + string? optionalArgument = null; + var result = true; + foreach (var argument in _positionalArguments) + { + if (multiValueArgument != null) + { + _context.ReportDiagnostic(Diagnostics.PositionalArgumentAfterMultiValue(argument.Member, multiValueArgument)); + result = false; + } + + if (argument.IsRequired && optionalArgument != null) + { + _context.ReportDiagnostic(Diagnostics.PositionalRequiredArgumentAfterOptional(argument.Member, optionalArgument)); + result = false; + } + + if (!argument.IsRequired) + { + optionalArgument = argument.Member.Name; + } + + if (argument.IsMultiValue) + { + multiValueArgument = argument.Member.Name; + } + } + + return result; + } + + private void CheckIgnoredDictionaryAttribute(ISymbol member, bool isDictionary, AttributeData? converter, AttributeData? attribute) + { + if (attribute == null) + { + return; + } + + if (!isDictionary) + { + _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForNonDictionary(member, attribute)); + } + else if (converter != null) + { + _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForDictionaryWithConverter(member, attribute)); + } + } + + private string? GetInitializerValue(IPropertySymbol symbol) + { + var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(_context.CancellationToken) as PropertyDeclarationSyntax; + if (syntax?.Initializer == null) + { + return null; + } + + var expression = syntax.Initializer.Value; + if (expression is PostfixUnaryExpressionSyntax postfixUnaryExpression) + { + if (postfixUnaryExpression.Kind() == SyntaxKind.SuppressNullableWarningExpression) + { + expression = postfixUnaryExpression.Operand; + } + } + + var expressionString = expression switch + { + // We have to include the type in a default expression because it's going to be + // assigned to an object so just "default" would always be null. + LiteralExpressionSyntax value => value.IsKind(SyntaxKind.DefaultLiteralExpression) ? $"default({symbol.Type.ToQualifiedName()})" : value?.Token.ToFullString(), + MemberAccessExpressionSyntax memberAccessExpression => GetSymbolExpressionString(memberAccessExpression), + IdentifierNameSyntax identifierName => GetSymbolExpressionString(identifierName), + _ => null, + }; + + if (expressionString == null) + { + _context.ReportDiagnostic(Diagnostics.UnsupportedInitializerSyntax(symbol, syntax.Initializer.GetLocation())); + } + + return expressionString; + } + + private string? GetSymbolExpressionString(ExpressionSyntax syntax) + { + var model = _compilation.GetSemanticModel(syntax.SyntaxTree); + var symbol = model.GetSymbolInfo(syntax); + return symbol.Symbol?.ToQualifiedName(); + } +} diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs new file mode 100644 index 00000000..a057a139 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -0,0 +1,171 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +[Generator] +public class ParserIncrementalGenerator : IIncrementalGenerator +{ + private enum ClassKind + { + Arguments, + CommandManager, + Command, + } + + private record struct ClassInfo(ClassDeclarationSyntax Syntax, ClassKind ClassKind); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, + static (ctx, _) => GetClassToGenerate(ctx) + ) + .Where(static c => c != null); + + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Left, source.Right!, spc)); + } + + private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) + { + if (classes.IsDefaultOrEmpty) + { + return; + } + + var typeHelper = new TypeHelper(compilation); + var converterGenerator = new ConverterGenerator(typeHelper, context); + var commandGenerator = new CommandGenerator(typeHelper, context); + foreach (var cls in classes) + { + var info = cls!.Value; + var syntax = info.Syntax; + context.CancellationToken.ThrowIfCancellationRequested(); + var languageVersion = (info.Syntax.SyntaxTree.Options as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.CSharp1; + var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(syntax, context.CancellationToken) is not INamedTypeSymbol symbol) + { + continue; + } + + // If this is a command without the GeneratedParserAttribute, add it and do nothing + // else. + if (info.ClassKind == ClassKind.Command) + { + if (symbol.ImplementsInterface(typeHelper.ICommand)) + { + commandGenerator.AddCommand(symbol); + } + else + { + // The other way around (interface without attribute) doesn't need a warning + // since it could be a base class for a command. + context.ReportDiagnostic(Diagnostics.CommandAttributeWithoutInterface(symbol)); + } + + continue; + } + + var attributeName = info.ClassKind == ClassKind.CommandManager + ? typeHelper.GeneratedCommandManagerAttribute!.Name + : typeHelper.GeneratedParserAttribute!.Name; + + if (languageVersion < LanguageVersion.CSharp8) + { + context.ReportDiagnostic(Diagnostics.UnsupportedLanguageVersion(symbol, attributeName)); + continue; + } + + if (!symbol.IsReferenceType) + { + context.ReportDiagnostic(Diagnostics.TypeNotReferenceType(symbol, attributeName)); + continue; + } + + if (!syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic(Diagnostics.ClassNotPartial(symbol, attributeName)); + continue; + } + + if (symbol.IsGenericType) + { + context.ReportDiagnostic(Diagnostics.ClassIsGeneric(symbol, attributeName)); + continue; + } + + if (symbol.ContainingType != null) + { + context.ReportDiagnostic(Diagnostics.ClassIsNested(symbol, attributeName)); + continue; + } + + if (info.ClassKind == ClassKind.CommandManager) + { + commandGenerator.AddManager(symbol); + continue; + } + + var source = ParserGenerator.Generate(context, symbol, typeHelper, converterGenerator, commandGenerator, + languageVersion); + + if (source != null) + { + context.AddSource(symbol.ToDisplayString().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); + } + } + + var converterSource = converterGenerator.Generate(); + if (converterSource != null) + { + context.AddSource("GeneratedConverters.g.cs", SourceText.From(converterSource, Encoding.UTF8)); + } + + commandGenerator.Generate(); + } + + private static ClassInfo? GetClassToGenerate(GeneratorSyntaxContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + var typeHelper = new TypeHelper(context.SemanticModel.Compilation); + var generatedParserType = typeHelper.GeneratedParserAttribute; + var GeneratedCommandManagerType = typeHelper.GeneratedCommandManagerAttribute; + var commandType = typeHelper.CommandAttribute; + var isCommand = false; + foreach (var attributeList in classDeclaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol) + { + // No symbol for the attribute for some reason. + continue; + } + + var attributeType = attributeSymbol.ContainingType; + if (attributeType.SymbolEquals(generatedParserType)) + { + return new(classDeclaration, ClassKind.Arguments); + } + + if (attributeType.SymbolEquals(GeneratedCommandManagerType)) + { + return new(classDeclaration, ClassKind.CommandManager); + } + + if (attributeType.SymbolEquals(commandType)) + { + isCommand = true; + } + } + } + + return isCommand ? new(classDeclaration, ClassKind.Command) : null; + } +} diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs new file mode 100644 index 00000000..7b474f6a --- /dev/null +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -0,0 +1,819 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Ookii.CommandLine.Generator.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Ookii.CommandLine.Generator.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The AliasAttribute is ignored on the argument defined by {0} because it has no long name.. + /// + internal static string AliasWithoutLongNameMessageFormat { + get { + return ResourceManager.GetString("AliasWithoutLongNameMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The AliasAttribute is ignored on an argument with no long name.. + /// + internal static string AliasWithoutLongNameTitle { + get { + return ResourceManager.GetString("AliasWithoutLongNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line argument defined by {0} uses the ArgumentConverterAttribute with a string parameter, which is not supported by the GeneratedParserAttribute. Use a Type parameter instead by using the typeof keyword.. + /// + internal static string ArgumentConverterStringNotSupportedMessageFormat { + get { + return ResourceManager.GetString("ArgumentConverterStringNotSupportedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ArgumentConverterAttribute must use the typeof keyword.. + /// + internal static string ArgumentConverterStringNotSupportedTitle { + get { + return ResourceManager.GetString("ArgumentConverterStringNotSupportedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument name '{0}' defined by '{1}' starts with a number, which cannot be used with the '-' argument prefix since it will be interpreted as a negative number.. + /// + internal static string ArgumentStartsWithNumberMessageFormat { + get { + return ResourceManager.GetString("ArgumentStartsWithNumberMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Argument names starting with a number cannot be used with the '-' prefix.. + /// + internal static string ArgumentStartsWithNumberTitle { + get { + return ResourceManager.GetString("ArgumentStartsWithNumberTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument defined by {0} should have a description for the usage help. Use the System.ComponentModel.DescriptionAttribute attribute on the member to supply a description.. + /// + internal static string ArgumentWithoutDescriptionMessageFormat { + get { + return ResourceManager.GetString("ArgumentWithoutDescriptionMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arguments should have a description.. + /// + internal static string ArgumentWithoutDescriptionTitle { + get { + return ResourceManager.GetString("ArgumentWithoutDescriptionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The class {0} may not be a generic class when the {1} attribute is used.. + /// + internal static string ClassIsGenericMessageFormat { + get { + return ResourceManager.GetString("ClassIsGenericMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The class may not be a generic type.. + /// + internal static string ClassIsGenericTitle { + get { + return ResourceManager.GetString("ClassIsGenericTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The class {0} may not be nested in another type when the {1} attribute is used.. + /// + internal static string ClassIsNestedMessageFormat { + get { + return ResourceManager.GetString("ClassIsNestedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The class may not be a nested type.. + /// + internal static string ClassIsNestedTitle { + get { + return ResourceManager.GetString("ClassIsNestedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The class {0} must use the 'partial' modifier when the {1} attribute is used.. + /// + internal static string ClassNotPartialMessageFormat { + get { + return ResourceManager.GetString("ClassNotPartialMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The class must be a partial class.. + /// + internal static string ClassNotPartialTitle { + get { + return ResourceManager.GetString("ClassNotPartialTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line arguments class {0} has the CommandAttribute but does not implement the ICommand interface.. + /// + internal static string CommandAttributeWithoutInterfaceMessageFormat { + get { + return ResourceManager.GetString("CommandAttributeWithoutInterfaceMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line arguments class has the CommandAttribute but does not implement the ICommand interface.. + /// + internal static string CommandAttributeWithoutInterfaceTitle { + get { + return ResourceManager.GetString("CommandAttributeWithoutInterfaceTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line arguments class {0} implements the ICommand interface but does not have the CommandAttribute attribute.. + /// + internal static string CommandInterfaceWithoutAttributeMessageFormat { + get { + return ResourceManager.GetString("CommandInterfaceWithoutAttributeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line arguments class implements the ICommand interface but does not have the CommandAttribute attribute.. + /// + internal static string CommandInterfaceWithoutAttributeTitle { + get { + return ResourceManager.GetString("CommandInterfaceWithoutAttributeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The subcommand defined by {0} should have a description for the usage help. Use the System.ComponentModel.DescriptionAttribute attribute on the class to supply a description.. + /// + internal static string CommandWithoutDescriptionMessageFormat { + get { + return ResourceManager.GetString("CommandWithoutDescriptionMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subcommands should have a description.. + /// + internal static string CommandWithoutDescriptionTitle { + get { + return ResourceManager.GetString("CommandWithoutDescriptionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The default value is ignored if the argument is required, multi-value, or a method argument.. + /// + internal static string DefaultValueIgnoredTitle { + get { + return ResourceManager.GetString("DefaultValueIgnoredTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The default value of the argument defined by {0} is ignored because it is a method argument.. + /// + internal static string DefaultValueWithMethodMessageFormat { + get { + return ResourceManager.GetString("DefaultValueWithMethodMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The default value of the argument defined by {0} is ignored because it is a multi-value argument.. + /// + internal static string DefaultValueWithMultiValueMessageFormat { + get { + return ResourceManager.GetString("DefaultValueWithMultiValueMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The default value of the argument defined by {0} is ignored because the argument is required.. + /// + internal static string DefaultValueWithRequiredMessageFormat { + get { + return ResourceManager.GetString("DefaultValueWithRequiredMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument defined by {0} uses the same position value as {1}.. + /// + internal static string DuplicatePositionMessageFormat { + get { + return ResourceManager.GetString("DuplicatePositionMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Two or more arguments use the same position value.. + /// + internal static string DuplicatePositionTitle { + get { + return ResourceManager.GetString("DuplicatePositionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command class {0} cannot use the GeneratedParserAttribute class, because it implements the ICommandWithCustomParsing interface.. + /// + internal static string GeneratedCustomParsingCommandMessageFormat { + get { + return ResourceManager.GetString("GeneratedCustomParsingCommandMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The GeneratedParserAttribute cannot be used with a class that implements the ICommandWithCustomParsing interface.. + /// + internal static string GeneratedCustomParsingCommandTitle { + get { + return ResourceManager.GetString("GeneratedCustomParsingCommandTitle", 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.. + /// + internal static string IgnoredAttributeForDictionaryWithConverterMessageFormat { + get { + return ResourceManager.GetString("IgnoredAttributeForDictionaryWithConverterMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The attribute is not used for a dictionary argument that has the ArgumentConverterAttribute attribute.. + /// + internal static string IgnoredAttributeForDictionaryWithConverterTitle { + get { + return ResourceManager.GetString("IgnoredAttributeForDictionaryWithConverterTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The attribute '{0}' on '{1}' will be ignored because '{1}' is not a subcommand.. + /// + internal static string IgnoredAttributeForNonCommandMessageFormat { + get { + return ResourceManager.GetString("IgnoredAttributeForNonCommandMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The attribute is not used for command line arguments classes that are not subcommands.. + /// + internal static string IgnoredAttributeForNonCommandTitle { + get { + return ResourceManager.GetString("IgnoredAttributeForNonCommandTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} attribute is ignored for the non-dictionary argument defined by {1}.. + /// + internal static string IgnoredAttributeForNonDictionaryMessageFormat { + get { + return ResourceManager.GetString("IgnoredAttributeForNonDictionaryMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The attribute is not used for a non-dictionary argument.. + /// + internal static string IgnoredAttributeForNonDictionaryTitle { + get { + return ResourceManager.GetString("IgnoredAttributeForNonDictionaryTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} attribute is ignored for the non-multi-value argument defined by {1}.. + /// + internal static string IgnoredAttributeForNonMultiValueMessageFormat { + get { + return ResourceManager.GetString("IgnoredAttributeForNonMultiValueMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The attribute is not used for a non-multi-value argument.. + /// + internal static string IgnoredAttributeForNonMultiValueTitle { + get { + return ResourceManager.GetString("IgnoredAttributeForNonMultiValueTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ApplicationFriendlyNameAttribute on '{0}' is ignored because '{0}' is a subcommand. Use '[assembly: ApplicationFriendlyName(...)]' instead.. + /// + internal static string IgnoredFriendlyNameAttributeMessageFormat { + get { + return ResourceManager.GetString("IgnoredFriendlyNameAttributeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ApplicationFriendlyNameAttribute is ignored on a subcommand.. + /// + internal static string IgnoredFriendlyNameAttributeTitle { + get { + return ResourceManager.GetString("IgnoredFriendlyNameAttributeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The TypeConverterAttribute on '{0}' will be ignored by the CommandLineParser. Use the ArgumentConverterAttribute instead.. + /// + internal static string IgnoredTypeConverterAttributeMessageFormat { + get { + return ResourceManager.GetString("IgnoredTypeConverterAttributeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The TypeConverterAttribute will be ignored.. + /// + internal static string IgnoredTypeConverterAttributeTitle { + get { + return ResourceManager.GetString("IgnoredTypeConverterAttributeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The multi-value command line argument defined by {0} must have an array rank of one.. + /// + internal static string InvalidArrayRankMessageFormat { + get { + return ResourceManager.GetString("InvalidArrayRankMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A multi-value command line argument defined by an array properties must have an array rank of one.. + /// + internal static string InvalidArrayRankTitle { + get { + return ResourceManager.GetString("InvalidArrayRankTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The assembly name '{0}' is not valid.. + /// + internal static string InvalidAssemblyNameMessageFormat { + get { + return ResourceManager.GetString("InvalidAssemblyNameMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid assembly name.. + /// + internal static string InvalidAssemblyNameTitle { + get { + return ResourceManager.GetString("InvalidAssemblyNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value '{0}' is not a valid C# namespace name. The default namespace will be used instead.. + /// + internal static string InvalidGeneratedConverterNamespaceMessageFormat { + get { + return ResourceManager.GetString("InvalidGeneratedConverterNamespaceMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified namespace for generated converters is not valid.. + /// + internal static string InvalidGeneratedConverterNamespaceTitle { + get { + return ResourceManager.GetString("InvalidGeneratedConverterNamespaceTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The method {0} does not have a valid signature for a command line argument.. + /// + internal static string InvalidMethodSignatureMessageFormat { + get { + return ResourceManager.GetString("InvalidMethodSignatureMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A method command line argument has an invalid signature.. + /// + internal static string InvalidMethodSignatureTitle { + get { + return ResourceManager.GetString("InvalidMethodSignatureTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for the argument defined by {0} because it is positional or required.. + /// + internal static string IsHiddenWithPositionalOrRequiredMessageFormat { + get { + return ResourceManager.GetString("IsHiddenWithPositionalOrRequiredMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for positional or required arguments.. + /// + internal static string IsHiddenWithPositionalOrRequiredTitle { + get { + return ResourceManager.GetString("IsHiddenWithPositionalOrRequiredTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}. Arguments defined by a required property are always required.. + /// + internal static string IsRequiredWithRequiredPropertyMessageFormat { + get { + return ResourceManager.GetString("IsRequiredWithRequiredPropertyMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsRequired property is ignored for a required property.. + /// + internal static string IsRequiredWithRequiredPropertyTitle { + get { + return ResourceManager.GetString("IsRequiredWithRequiredPropertyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument defined by {0} has an explicit short name, so setting IsShort to false has no effect.. + /// + internal static string IsShortIgnoredMessageFormat { + get { + return ResourceManager.GetString("IsShortIgnoredMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IsShort is ignored if an explicit short name is set.. + /// + internal static string IsShortIgnoredTitle { + get { + return ResourceManager.GetString("IsShortIgnoredTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The arguments class '{0}' contains positional arguments using an explicit Position value, and ones using IsPositional for member-based ordering, which is not allowed. Note that this may include arguments defined by a base class.. + /// + internal static string MixedImplicitExplicitPositionsMessageFormat { + get { + return ResourceManager.GetString("MixedImplicitExplicitPositionsMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Positional arguments with an explicit position value and those with a position inferred from the member order cannot be mixed.. + /// + internal static string MixedImplicitExplicitPositionsTitle { + get { + return ResourceManager.GetString("MixedImplicitExplicitPositionsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No command line argument converter exists for type {0} used by the argument defined by {1}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. + /// + internal static string NoConverterMessageFormat { + get { + return ResourceManager.GetString("NoConverterMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No command line argument converter exists for the argument's type.. + /// + internal static string NoConverterTitle { + get { + return ResourceManager.GetString("NoConverterTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument defined by {0} has both IsLong and IsShort set to false, which means it has no name if long/short mode is used.. + /// + internal static string NoLongOrShortNameMessageFormat { + get { + return ResourceManager.GetString("NoLongOrShortNameMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Argument has neither a long nor short name.. + /// + internal static string NoLongOrShortNameTitle { + get { + return ResourceManager.GetString("NoLongOrShortNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The property {0} will not create a command line argument because it is not a public instance property.. + /// + internal static string NonPublicInstancePropertyMessageFormat { + get { + return ResourceManager.GetString("NonPublicInstancePropertyMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties that are not public instance will be ignored.. + /// + internal static string NonPublicInstancePropertyTitle { + get { + return ResourceManager.GetString("NonPublicInstancePropertyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The method {0} will not create a command line argument because it is not a public static method.. + /// + internal static string NonPublicStaticMethodMessageFormat { + get { + return ResourceManager.GetString("NonPublicStaticMethodMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Methods that are not public and static will be ignored.. + /// + internal static string NonPublicStaticMethodTitle { + get { + return ResourceManager.GetString("NonPublicStaticMethodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line argument property {0} may only have an 'init' accessor if the property is also declared as 'required'.. + /// + internal static string NonRequiredInitOnlyPropertyMessageFormat { + get { + return ResourceManager.GetString("NonRequiredInitOnlyPropertyMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Init accessors may only be used on required properties.. + /// + internal static string NonRequiredInitOnlyPropertyTitle { + get { + return ResourceManager.GetString("NonRequiredInitOnlyPropertyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The subcommand defined by {0} uses the ParentCommandAttribute with a string parameter, which is not supported by the GeneratedParserAttribute. Use a Type parameter instead by using the typeof keyword.. + /// + internal static string ParentCommandStringNotSupportedMessageFormat { + get { + return ResourceManager.GetString("ParentCommandStringNotSupportedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ParentCommandAttribute must use the typeof keyword.. + /// + internal static string ParentCommandStringNotSupportedTitle { + get { + return ResourceManager.GetString("ParentCommandStringNotSupportedTitle", 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.. + /// + internal static string PositionalArgumentAfterMultiValueMessageFormat { + get { + return ResourceManager.GetString("PositionalArgumentAfterMultiValueMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A positional multi-value argument must be the last positional argument.. + /// + internal static string PositionalArgumentAfterMultiValueTitle { + get { + return ResourceManager.GetString("PositionalArgumentAfterMultiValueTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The required positional argument defined by {0} comes after {1}, which is optional.. + /// + internal static string PositionalRequiredArgumentAfterOptionalMessageFormat { + get { + return ResourceManager.GetString("PositionalRequiredArgumentAfterOptionalMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Required positional arguments must come before optional positional arguments.. + /// + internal static string PositionalRequiredArgumentAfterOptionalTitle { + get { + return ResourceManager.GetString("PositionalRequiredArgumentAfterOptionalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The property {0} must have a public set accessor.. + /// + internal static string PropertyIsReadOnlyMessageFormat { + get { + return ResourceManager.GetString("PropertyIsReadOnlyMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A command line argument property must have a public set accessor.. + /// + internal static string PropertyIsReadOnlyTitle { + get { + return ResourceManager.GetString("PropertyIsReadOnlyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ShortAliasAttribute is ignored on the argument defined by {0} because it has no short name.. + /// + internal static string ShortAliasWithoutShortNameMessageFormat { + get { + return ResourceManager.GetString("ShortAliasWithoutShortNameMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ShortAliasAttribute is ignored on an argument with no short name.. + /// + internal static string ShortAliasWithoutShortNameTitle { + get { + return ResourceManager.GetString("ShortAliasWithoutShortNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type {0} must be a reference type (class) when the {1} attribute is used.. + /// + internal static string TypeNotReferenceTypeMessageFormat { + get { + return ResourceManager.GetString("TypeNotReferenceTypeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command line arguments or command manager type must be a reference type.. + /// + internal static string TypeNotReferenceTypeTitle { + get { + return ResourceManager.GetString("TypeNotReferenceTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An assembly matching the name '{0}' was not found.. + /// + internal static string UnknownAssemblyNameMessageFormat { + get { + return ResourceManager.GetString("UnknownAssemblyNameMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown assembly name.. + /// + internal static string UnknownAssemblyNameTitle { + get { + return ResourceManager.GetString("UnknownAssemblyNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The initial value of the property '{0}' will not be included in the usage help because it is not a literal expression, enum value, constant, or property. Consider changing the initializer, or use CommandLineArgumentAttribute.DefaultValue as an alternative.. + /// + internal static string UnsupportedInitializerSyntaxMessageFormat { + get { + return ResourceManager.GetString("UnsupportedInitializerSyntaxMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The property's initial value uses an unsupported expression.. + /// + internal static string UnsupportedInitializerSyntaxTitle { + get { + return ResourceManager.GetString("UnsupportedInitializerSyntaxTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type '{0}' uses the '{1}' attribute, which requires at least C# 8.0.. + /// + internal static string UnsupportedLanguageVersionMessageFormat { + get { + return ResourceManager.GetString("UnsupportedLanguageVersionMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ookii.CommandLine source generation requires at least C# 8.0.. + /// + internal static string UnsupportedLanguageVersionTitle { + get { + return ResourceManager.GetString("UnsupportedLanguageVersionTitle", resourceCulture); + } + } + } +} diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx new file mode 100644 index 00000000..40bd3799 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The AliasAttribute is ignored on the argument defined by {0} because it has no long name. + + + The AliasAttribute is ignored on an argument with no long name. + + + The command line argument defined by {0} uses the ArgumentConverterAttribute with a string parameter, which is not supported by the GeneratedParserAttribute. Use a Type parameter instead by using the typeof keyword. + + + The ArgumentConverterAttribute must use the typeof keyword. + + + The argument name '{0}' defined by '{1}' starts with a number, which cannot be used with the '-' argument prefix since it will be interpreted as a negative number. + + + Argument names starting with a number cannot be used with the '-' prefix. + + + The argument defined by {0} should have a description for the usage help. Use the System.ComponentModel.DescriptionAttribute attribute on the member to supply a description. + + + Arguments should have a description. + + + The class {0} may not be a generic class when the {1} attribute is used. + + + The class may not be a generic type. + + + The class {0} may not be nested in another type when the {1} attribute is used. + + + The class may not be a nested type. + + + The class {0} must use the 'partial' modifier when the {1} attribute is used. + + + The class must be a partial class. + + + The command line arguments class {0} has the CommandAttribute but does not implement the ICommand interface. + + + The command line arguments class has the CommandAttribute but does not implement the ICommand interface. + + + The command line arguments class {0} implements the ICommand interface but does not have the CommandAttribute attribute. + + + The command line arguments class implements the ICommand interface but does not have the CommandAttribute attribute. + + + The subcommand defined by {0} should have a description for the usage help. Use the System.ComponentModel.DescriptionAttribute attribute on the class to supply a description. + + + Subcommands should have a description. + + + The default value is ignored if the argument is required, multi-value, or a method argument. + + + The default value of the argument defined by {0} is ignored because it is a method argument. + + + The default value of the argument defined by {0} is ignored because it is a multi-value argument. + + + The default value of the argument defined by {0} is ignored because the argument is required. + + + The argument defined by {0} uses the same position value as {1}. + + + Two or more arguments use the same position value. + + + The command class {0} cannot use the GeneratedParserAttribute class, because it implements the ICommandWithCustomParsing interface. + + + The GeneratedParserAttribute cannot be used with a class that implements the ICommandWithCustomParsing interface. + + + The {0} attribute is ignored for the dictionary argument defined by {1} that has the ArgumentConverterAttribute attribute. + + + The attribute is not used for a dictionary argument that has the ArgumentConverterAttribute attribute. + + + The attribute '{0}' on '{1}' will be ignored because '{1}' is not a subcommand. + + + The attribute is not used for command line arguments classes that are not subcommands. + + + The {0} attribute is ignored for the non-dictionary argument defined by {1}. + + + The attribute is not used for a non-dictionary argument. + + + The {0} attribute is ignored for the non-multi-value argument defined by {1}. + + + The attribute is not used for a non-multi-value argument. + + + The ApplicationFriendlyNameAttribute on '{0}' is ignored because '{0}' is a subcommand. Use '[assembly: ApplicationFriendlyName(...)]' instead. + + + The ApplicationFriendlyNameAttribute is ignored on a subcommand. + + + The TypeConverterAttribute on '{0}' will be ignored by the CommandLineParser. Use the ArgumentConverterAttribute instead. + + + The TypeConverterAttribute will be ignored. + + + The multi-value command line argument defined by {0} must have an array rank of one. + + + A multi-value command line argument defined by an array properties must have an array rank of one. + + + The assembly name '{0}' is not valid. + + + Invalid assembly name. + + + The value '{0}' is not a valid C# namespace name. The default namespace will be used instead. + + + The specified namespace for generated converters is not valid. + + + The method {0} does not have a valid signature for a command line argument. + + + A method command line argument has an invalid signature. + + + The CommandLineArgumentAttribute.IsHidden property is ignored for the argument defined by {0} because it is positional or required. + + + The CommandLineArgumentAttribute.IsHidden property is ignored for positional or required arguments. + + + The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}. Arguments defined by a required property are always required. + + + The CommandLineArgumentAttribute.IsRequired property is ignored for a required property. + + + The argument defined by {0} has an explicit short name, so setting IsShort to false has no effect. + + + IsShort is ignored if an explicit short name is set. + + + The arguments class '{0}' contains positional arguments using an explicit Position value, and ones using IsPositional for member-based ordering, which is not allowed. Note that this may include arguments defined by a base class. + + + Positional arguments with an explicit position value and those with a position inferred from the member order cannot be mixed. + + + No command line argument converter exists for type {0} used by the argument defined by {1}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. + + + No command line argument converter exists for the argument's type. + + + The argument defined by {0} has both IsLong and IsShort set to false, which means it has no name if long/short mode is used. + + + Argument has neither a long nor short name. + + + The property {0} will not create a command line argument because it is not a public instance property. + + + Properties that are not public instance will be ignored. + + + The method {0} will not create a command line argument because it is not a public static method. + + + Methods that are not public and static will be ignored. + + + The command line argument property {0} may only have an 'init' accessor if the property is also declared as 'required'. + + + Init accessors may only be used on required properties. + + + The subcommand defined by {0} uses the ParentCommandAttribute with a string parameter, which is not supported by the GeneratedParserAttribute. Use a Type parameter instead by using the typeof keyword. + + + The ParentCommandAttribute must use the typeof keyword. + + + The positional argument defined by {0} comes after {1}, which is a multi-value argument and must come last. + + + A positional multi-value argument must be the last positional argument. + + + The required positional argument defined by {0} comes after {1}, which is optional. + + + Required positional arguments must come before optional positional arguments. + + + The property {0} must have a public set accessor. + + + A command line argument property must have a public set accessor. + + + The ShortAliasAttribute is ignored on the argument defined by {0} because it has no short name. + + + The ShortAliasAttribute is ignored on an argument with no short name. + + + The type {0} must be a reference type (class) when the {1} attribute is used. + + + The command line arguments or command manager type must be a reference type. + + + An assembly matching the name '{0}' was not found. + + + Unknown assembly name. + + + The initial value of the property '{0}' will not be included in the usage help because it is not a literal expression, enum value, constant, or property. Consider changing the initializer, or use CommandLineArgumentAttribute.DefaultValue as an alternative. + + + The property's initial value uses an unsupported expression. + + + The type '{0}' uses the '{1}' attribute, which requires at least C# 8.0. + + + Ookii.CommandLine source generation requires at least C# 8.0. + + \ No newline at end of file diff --git a/src/Ookii.CommandLine.Generator/SourceBuilder.cs b/src/Ookii.CommandLine.Generator/SourceBuilder.cs new file mode 100644 index 00000000..be75d029 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/SourceBuilder.cs @@ -0,0 +1,109 @@ +using Microsoft.CodeAnalysis; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal class SourceBuilder +{ + private readonly StringBuilder _builder = new(); + private int _indentLevel; + private bool _startOfLine = true; + private bool _needArgumentSeparator; + + public SourceBuilder(INamespaceSymbol ns) + : this(ns.IsGlobalNamespace ? null : ns.ToDisplayString()) + { + } + + public SourceBuilder(string? ns) + { + _builder.AppendLine("// "); + _builder.AppendLine("#nullable enable"); + _builder.AppendLine(); + if (ns != null) + { + AppendLine($"namespace {ns}"); + OpenBlock(); + } + } + + public void Append(string text) + { + WriteIndent(); + _builder.Append(text); + _startOfLine = false; + } + + public void AppendLine() + { + _builder.AppendLine(); + _startOfLine = true; + } + + public void AppendLine(string text) + { + WriteIndent(); + _builder.AppendLine(text); + _startOfLine = true; + } + + public void AppendArgument(string text) + { + if (_needArgumentSeparator) + { + AppendLine(","); + } + + Append(text); + _needArgumentSeparator = true; + } + + public void CloseArgumentList(bool withSemicolon = true) + { + if (withSemicolon) + { + AppendLine(");"); + } + else + { + AppendLine(")"); + } + + --_indentLevel; + _needArgumentSeparator = false; + } + + public void OpenBlock() + { + AppendLine("{"); + ++_indentLevel; + } + + public void CloseBlock() + { + --_indentLevel; + AppendLine("}"); + } + + public string GetSource() + { + while (_indentLevel > 0) + { + CloseBlock(); + } + + return _builder.ToString(); + } + + public void IncreaseIndent() => ++_indentLevel; + + public void DecreaseIndent() => --_indentLevel; + + private void WriteIndent() + { + if (_startOfLine) + { + _builder.Append(' ', _indentLevel * 4); + } + } +} diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs new file mode 100644 index 00000000..197e00a2 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; + +namespace Ookii.CommandLine.Generator; + +internal class TypeHelper +{ + private readonly Compilation _compilation; + private const string NamespacePrefix = "Ookii.CommandLine."; + + public TypeHelper(Compilation compilation) + { + _compilation = compilation; + } + + public Compilation Compilation => _compilation; + + public INamedTypeSymbol Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); + + public INamedTypeSymbol Char => _compilation.GetSpecialType(SpecialType.System_Char); + + public INamedTypeSymbol? Dictionary => _compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName); + + public INamedTypeSymbol? IDictionary => _compilation.GetTypeByMetadataName(typeof(IDictionary<,>).FullName); + + public INamedTypeSymbol? ICollection => _compilation.GetTypeByMetadataName(typeof(ICollection<>).FullName); + + public INamedTypeSymbol? DescriptionAttribute => _compilation.GetTypeByMetadataName(typeof(DescriptionAttribute).FullName); + + public INamedTypeSymbol? AssemblyDescriptionAttribute => _compilation.GetTypeByMetadataName(typeof(AssemblyDescriptionAttribute).FullName); + + public INamedTypeSymbol? TypeConverterAttribute => _compilation.GetTypeByMetadataName(typeof(TypeConverterAttribute).FullName); + + public INamedTypeSymbol? ISpanParsable => _compilation.GetTypeByMetadataName("System.ISpanParsable`1"); + + public INamedTypeSymbol? IParsable => _compilation.GetTypeByMetadataName("System.IParsable`1"); + + public INamedTypeSymbol? ReadOnlySpan => _compilation.GetTypeByMetadataName("System.ReadOnlySpan`1"); + + public INamedTypeSymbol? ReadOnlySpanOfChar => ReadOnlySpan?.Construct(Char); + + public INamedTypeSymbol? CultureInfo => _compilation.GetTypeByMetadataName(typeof(CultureInfo).FullName); + + public INamedTypeSymbol? CommandLineParser => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineParser"); + + public INamedTypeSymbol? IParser => _compilation.GetTypeByMetadataName(NamespacePrefix + "IParserProvider`1"); + + public INamedTypeSymbol? GeneratedParserAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "GeneratedParserAttribute"); + + public INamedTypeSymbol? CommandLineArgumentAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineArgumentAttribute"); + + public INamedTypeSymbol? ParseOptionsAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ParseOptionsAttribute"); + + public INamedTypeSymbol? ApplicationFriendlyNameAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ApplicationFriendlyNameAttribute"); + + public INamedTypeSymbol? MultiValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "MultiValueSeparatorAttribute"); + + public INamedTypeSymbol? AllowDuplicateDictionaryKeysAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "AllowDuplicateDictionaryKeysAttribute"); + + public INamedTypeSymbol? AliasAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "AliasAttribute"); + + public INamedTypeSymbol? ShortAliasAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ShortAliasAttribute"); + + public INamedTypeSymbol? ValueDescriptionAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ValueDescriptionAttribute"); + + public INamedTypeSymbol? CancelMode => _compilation.GetTypeByMetadataName(NamespacePrefix + "CancelMode"); + + public INamedTypeSymbol? ArgumentValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ArgumentValidationAttribute"); + + public INamedTypeSymbol? ClassValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ClassValidationAttribute"); + + public INamedTypeSymbol? KeyValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.KeyValueSeparatorAttribute"); + + public INamedTypeSymbol? ArgumentConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.ArgumentConverterAttribute" + ); + public INamedTypeSymbol? KeyConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.KeyConverterAttribute"); + + public INamedTypeSymbol? ValueConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.ValueConverterAttribute"); + + public INamedTypeSymbol? GeneratedConverterNamespaceAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.GeneratedConverterNamespaceAttribute"); + + public INamedTypeSymbol? CommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.CommandAttribute"); + + public INamedTypeSymbol? GeneratedCommandManagerAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandManagerAttribute"); + + public INamedTypeSymbol? ICommand => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommand"); + + public INamedTypeSymbol? ICommandWithCustomParsing => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandWithCustomParsing"); + + public INamedTypeSymbol? ICommandProvider => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandProvider"); + + public INamedTypeSymbol? ParentCommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ParentCommandAttribute"); + +} diff --git a/src/Ookii.CommandLine.Generator/ookii.snk b/src/Ookii.CommandLine.Generator/ookii.snk new file mode 100644 index 00000000..1befa134 Binary files /dev/null and b/src/Ookii.CommandLine.Generator/ookii.snk differ diff --git a/src/Ookii.CommandLine.Tests.Commands/Commands.cs b/src/Ookii.CommandLine.Tests.Commands/Commands.cs new file mode 100644 index 00000000..5d9458a2 --- /dev/null +++ b/src/Ookii.CommandLine.Tests.Commands/Commands.cs @@ -0,0 +1,30 @@ +// Commands to test loading commands from an external assembly. +using Ookii.CommandLine.Commands; + +namespace Ookii.CommandLine.Tests.Commands; + +#pragma warning disable OCL0034 // Subcommands should have a description. + +[Command("external")] +[GeneratedParser] +public partial class ExternalCommand : ICommand +{ + public int Run() => throw new NotImplementedException(); +} + +[Command] +public class OtherExternalCommand : ICommand +{ + public int Run() => throw new NotImplementedException(); +} + +[Command] +internal class InternalCommand : ICommand +{ + public int Run() => throw new NotImplementedException(); +} + +public class NotACommand : ICommand +{ + public int Run() => throw new NotImplementedException(); +} diff --git a/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj new file mode 100644 index 00000000..bafddff5 --- /dev/null +++ b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj @@ -0,0 +1,20 @@ + + + + net7.0;net6.0;net48 + enable + enable + 11.0 + true + ookii.snk + false + + 1.0.0 + + + + + + + + diff --git a/src/Ookii.CommandLine.Tests.Commands/ookii.snk b/src/Ookii.CommandLine.Tests.Commands/ookii.snk new file mode 100644 index 00000000..1befa134 Binary files /dev/null and b/src/Ookii.CommandLine.Tests.Commands/ookii.snk differ diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index fe2a7dd8..c946f370 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -1,4 +1,5 @@ -using Ookii.CommandLine.Validation; +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -7,558 +8,677 @@ using System.IO; using System.Net; -namespace Ookii.CommandLine.Tests +// Nullability is disabled for this file because there are some differences for both reflection and +// source generation in how nullable and non-nullable contexts are handled and both need to be +// tested. +#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 + +namespace Ookii.CommandLine.Tests; + +[GeneratedParser] +partial class EmptyArguments { - class EmptyArguments +} + +[GeneratedParser] +[ApplicationFriendlyName("Friendly name")] +[Description("Test arguments description.")] +partial class TestArguments +{ + private readonly Collection _arg12 = new Collection(); + private readonly Dictionary _arg14 = new Dictionary(); + + [CommandLineArgument("arg1", Position = 1, IsRequired = true)] + [Description("Arg1 description.")] + public string Arg1 { get; set; } + + [CommandLineArgument("other", Position = 2, DefaultValue = 42)] + [ValueDescription("Number")] + [Description("Arg2 description.")] + public int Arg2 { get; set; } + + [CommandLineArgument("notSwitch", Position = 3, DefaultValue = false)] + public bool NotSwitch { get; set; } + + [CommandLineArgument()] + public string Arg3 { get; set; } + + // Default value is intentionally a string to test default value conversion. + [CommandLineArgument("other2", DefaultValue = "47", Position = 5), Description("Arg4 description.")] + [ValueDescription("Number")] + [ValidateRange(0, 1000, IncludeInUsageHelp = false)] + [ArgumentConverter(typeof(WrappedDefaultTypeConverter))] + public int Arg4 { get; set; } + + // Short/long name stuff should be ignored if not using LongShort mode. + [CommandLineArgument(Position = 4, ShortName = 'a', IsLong = false, DefaultValue = 1.0f, IncludeDefaultInUsageHelp = false)] + [Description("Arg5 description.")] + public float Arg5 { get; set; } + + [Alias("Alias1")] + [Alias("Alias2")] + [CommandLineArgument(IsRequired = true), Description("Arg6 description.")] + public string Arg6 { get; set; } + + [Alias("Alias3")] + [CommandLineArgument()] + public bool Arg7 { get; set; } + + [CommandLineArgument(Position = 6)] + public DayOfWeek[] Arg8 { get; set; } + + [CommandLineArgument()] + [ValidateRange(0, 100)] + public int? Arg9 { get; set; } + + [CommandLineArgument] + public bool[] Arg10 { get; set; } + + [CommandLineArgument] + public bool? Arg11 { get; set; } + + [CommandLineArgument(DefaultValue = 42)] // Default value is ignored for collection types. + public Collection Arg12 { + get { return _arg12; } } - [ApplicationFriendlyName("Friendly name")] - [Description("Test arguments description.")] - class TestArguments + [CommandLineArgument] + public Dictionary Arg13 { get; set; } + + [CommandLineArgument] + public IDictionary Arg14 { - private readonly Collection _arg12 = new Collection(); - private readonly Dictionary _arg14 = new Dictionary(); + get { return _arg14; } + } - private TestArguments(string notAnArg) - { - } + [CommandLineArgument, ArgumentConverter(typeof(KeyValuePairConverter))] + public KeyValuePair Arg15 { get; set; } + + public string NotAnArg { get; set; } + + [CommandLineArgument()] + private string NotAnArg2 { get; set; } - public TestArguments([Description("Arg1 description.")] string arg1, [Description("Arg2 description."), ArgumentName("other"), ValueDescription("Number")] int arg2 = 42, bool notSwitch = false) + [CommandLineArgument()] + public static string NotAnArg3 { get; set; } +} + +[GeneratedParser] +partial class ThrowingArguments +{ + private int _throwingArgument; + + [CommandLineArgument(Position = 0)] + public string Arg { get; set; } + + [CommandLineArgument] + public int ThrowingArgument + { + get { return _throwingArgument; } + set { - Arg1 = arg1; - Arg2 = arg2; - NotSwitch = notSwitch; + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _throwingArgument = value; } + } +} - public string Arg1 { get; private set; } +[GeneratedParser] +partial class ThrowingConstructor +{ + public ThrowingConstructor() + { + throw new ArgumentException(); + } - public int Arg2 { get; private set; } + [CommandLineArgument] + public int Arg { get; set; } +} - public bool NotSwitch { get; private set; } +[GeneratedParser] +partial class DictionaryArguments +{ + [CommandLineArgument] + public Dictionary NoDuplicateKeys { get; set; } + [CommandLineArgument, AllowDuplicateDictionaryKeys] + public Dictionary DuplicateKeys { get; set; } +} - [CommandLineArgument()] - public string Arg3 { get; set; } +[GeneratedParser] +partial class MultiValueSeparatorArguments +{ + [CommandLineArgument] + public string[] NoSeparator { get; set; } + [CommandLineArgument, MultiValueSeparator(",")] + public string[] Separator { get; set; } +} - // Default value is intentionally a string to test default value conversion. - [CommandLineArgument("other2", DefaultValue = "47", ValueDescription = "Number", Position = 1), Description("Arg4 description.")] - [ValidateRange(0, 1000, IncludeInUsageHelp = false)] - public int Arg4 { get; set; } +[GeneratedParser] +partial class SimpleArguments +{ + [CommandLineArgument] + public string Argument1 { get; set; } + [CommandLineArgument] + public string Argument2 { get; set; } +} - // Short/long name stuff should be ignored if not using LongShort mode. - [CommandLineArgument(Position = 0, ShortName = 'a', IsLong = false), Description("Arg5 description.")] - public float Arg5 { get; set; } +[GeneratedParser] +partial class KeyValueSeparatorArguments +{ + [CommandLineArgument] + public Dictionary DefaultSeparator { get; set; } - [Alias("Alias1")] - [Alias("Alias2")] - [CommandLineArgument(IsRequired = true), Description("Arg6 description.")] - public string Arg6 { get; set; } + [CommandLineArgument] + [KeyValueSeparator("<=>")] + public Dictionary CustomSeparator { get; set; } +} - [Alias("Alias3")] - [CommandLineArgument()] - public bool Arg7 { get; set; } +[GeneratedParser] +partial class CancelArguments +{ + [CommandLineArgument] + public string Argument1 { get; set; } - [CommandLineArgument(Position = 2)] - public DayOfWeek[] Arg8 { get; set; } + [CommandLineArgument] + public string Argument2 { get; set; } - [CommandLineArgument()] - [ValidateRange(0, 100)] - public int? Arg9 { get; set; } + [CommandLineArgument] + public bool DoesNotCancel { get; set; } - [CommandLineArgument] - public bool[] Arg10 { get; set; } + [CommandLineArgument(CancelParsing = CancelMode.Abort)] + public bool DoesCancel { get; set; } - [CommandLineArgument] - public bool? Arg11 { get; set; } + [CommandLineArgument(CancelParsing = CancelMode.Success)] + public bool DoesCancelWithSuccess { get; set; } +} - [CommandLineArgument(DefaultValue = 42)] // Default value is ignored for collection types. - public Collection Arg12 - { - get { return _arg12; } - } +[GeneratedParser] +[ParseOptions( + Mode = ParsingMode.LongShort, + DuplicateArguments = ErrorMode.Allow, + AllowWhiteSpaceValueSeparator = false, + ArgumentNamePrefixes = new[] { "--", "-" }, + LongArgumentNamePrefix = "---", + CaseSensitive = true, + NameValueSeparators = new[] { '=' }, + AutoHelpArgument = false)] +partial class ParseOptionsArguments +{ + [CommandLineArgument] + public string Argument { get; set; } +} - [CommandLineArgument] - public Dictionary Arg13 { get; set; } +[GeneratedParser] +partial class CultureArguments +{ + [CommandLineArgument] + public float Argument { get; set; } +} - [CommandLineArgument] - public IDictionary Arg14 - { - get { return _arg14; } - } +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class LongShortArguments +{ + [CommandLineArgument, ShortAlias('c')] + [Description("Arg1 description.")] + public int Arg1 { get; set; } - [CommandLineArgument, TypeConverter(typeof(KeyValuePairConverter))] - public KeyValuePair Arg15 { get; set; } + [CommandLineArgument(ShortName = 'a', Position = 2), ShortAlias('b'), Alias("baz")] + [Description("Arg2 description.")] + public int Arg2 { get; set; } - public string NotAnArg { get; set; } + [CommandLineArgument(IsShort = true)] + [Description("Switch1 description.")] + public bool Switch1 { get; set; } - [CommandLineArgument()] - private string NotAnArg2 { get; set; } + [CommandLineArgument(ShortName = 'k')] + [Description("Switch2 description.")] + public bool Switch2 { get; set; } - [CommandLineArgument()] - public static string NotAnArg3 { get; set; } - } + [CommandLineArgument(ShortName = 'u', IsLong = false)] + [Description("Switch3 description.")] + public bool Switch3 { get; set; } - class MultipleConstructorsArguments - { - private int _throwingArgument; + [CommandLineArgument("foo", Position = 0, IsShort = true, DefaultValue = 0)] + [Description("Foo description.")] + public int Foo { get; set; } - public MultipleConstructorsArguments() { } - public MultipleConstructorsArguments(string notArg1, int notArg2) { } - [CommandLineConstructor] - public MultipleConstructorsArguments(string arg1) - { - if (arg1 == "invalid") - { - throw new ArgumentException("Invalid argument value.", nameof(arg1)); - } - } + [CommandLineArgument("bar", DefaultValue = 0, Position = 1)] + [Description("Bar description.")] + public int Bar { get; set; } +} - [CommandLineArgument] - public int ThrowingArgument - { - get { return _throwingArgument; } - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } +[GeneratedParser] +partial class MethodArguments +{ + // Using method arguments to store stuff in static fields isn't really recommended. It's + // done here for testing purposes only. + public static string CalledMethodName; + public static int Value; - _throwingArgument = value; - } - } + [CommandLineArgument] + public static bool NoCancel() + { + CalledMethodName = nameof(NoCancel); + return true; } - class DictionaryArguments + [CommandLineArgument] + public static bool Cancel() { - [CommandLineArgument] - public Dictionary NoDuplicateKeys { get; set; } - [CommandLineArgument, AllowDuplicateDictionaryKeys] - public Dictionary DuplicateKeys { get; set; } + CalledMethodName = nameof(Cancel); + return false; } - class MultiValueSeparatorArguments + [CommandLineArgument] + public static CancelMode CancelModeAbort() { - [CommandLineArgument] - public string[] NoSeparator { get; set; } - [CommandLineArgument, MultiValueSeparator(",")] - public string[] Separator { get; set; } + CalledMethodName = nameof(CancelModeAbort); + return CancelMode.Abort; } - class SimpleArguments + [CommandLineArgument] + public static CancelMode CancelModeSuccess() { - [CommandLineArgument] - public string Argument1 { get; set; } - [CommandLineArgument] - public string Argument2 { get; set; } + CalledMethodName = nameof(CancelModeSuccess); + return CancelMode.Success; } - class KeyValueSeparatorArguments + [CommandLineArgument] + public static CancelMode CancelModeNone() { - [CommandLineArgument] - public Dictionary DefaultSeparator { get; set; } - - [CommandLineArgument] - [KeyValueSeparator("<=>")] - public Dictionary CustomSeparator { get; set; } + CalledMethodName = nameof(CancelModeNone); + return CancelMode.None; } - class CancelArguments + [CommandLineArgument] + public static bool CancelWithHelp(CommandLineParser parser) { - [CommandLineArgument] - public string Argument1 { get; set; } + CalledMethodName = nameof(CancelWithHelp); + parser.HelpRequested = true; + return false; + } - [CommandLineArgument] - public string Argument2 { get; set; } + [CommandLineArgument] + public static bool CancelWithValue(int value) + { + CalledMethodName = nameof(CancelWithValue); + Value = value; + return value > 0; + } - [CommandLineArgument] - public bool DoesNotCancel { get; set; } + [CommandLineArgument] + public static bool CancelWithValueAndHelp(int value, CommandLineParser parser) + { + CalledMethodName = nameof(CancelWithValueAndHelp); + Value = value; + // This should be reset to false if parsing continues. + parser.HelpRequested = true; + return value > 0; + } - [CommandLineArgument(CancelParsing = true)] - public bool DoesCancel { get; set; } + [CommandLineArgument] + public static void NoReturn() + { + CalledMethodName = nameof(NoReturn); } - [ParseOptions( - Mode = ParsingMode.LongShort, - DuplicateArguments = ErrorMode.Allow, - AllowWhiteSpaceValueSeparator = false, - ArgumentNamePrefixes = new[] { "--", "-" }, - LongArgumentNamePrefix = "---", - CaseSensitive = true, - NameValueSeparator = '=', - AutoHelpArgument = false)] - class ParseOptionsArguments + [CommandLineArgument(Position = 0)] + public static void Positional(int value) { - [CommandLineArgument] - public string Argument { get; set; } + CalledMethodName = nameof(Positional); + Value = value; } - class CultureArguments + [CommandLineArgument] + public void NoStatic() { - [CommandLineArgument] - public float Argument { get; set; } } - [ParseOptions(Mode = ParsingMode.LongShort)] - class LongShortArguments + [CommandLineArgument] + private static void NotPublic() { - public LongShortArguments([ArgumentName(IsShort = true), Description("Foo description.")] int foo = 0, - [Description("Bar description.")] int bar = 0) - { - Foo = foo; - Bar = bar; - } + } - [CommandLineArgument, ShortAlias('c')] - [Description("Arg1 description.")] - public int Arg1 { get; set; } + public static void NotAnArgument() + { + } +} - [CommandLineArgument(ShortName = 'a', Position = 0), ShortAlias('b'), Alias("baz")] - [Description("Arg2 description.")] - public int Arg2 { get; set; } +[GeneratedParser] +partial class AutomaticConflictingNameArguments +{ + [CommandLineArgument] + public int Help { get; set; } - [CommandLineArgument(IsShort = true)] - [Description("Switch1 description.")] - public bool Switch1 { get; set; } + [CommandLineArgument] + public int Version { get; set; } +} - [CommandLineArgument(ShortName = 'k')] - [Description("Switch2 description.")] - public bool Switch2 { get; set; } +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class AutomaticConflictingShortNameArguments +{ + [CommandLineArgument(ShortName = '?')] + public int Foo { get; set; } +} - [CommandLineArgument(ShortName = 'u', IsLong = false)] - [Description("Switch3 description.")] - public bool Switch3 { get; set; } +[GeneratedParser] +partial class HiddenArguments +{ + [CommandLineArgument] + public int Foo { get; set; } - public int Foo { get; set; } + [CommandLineArgument(IsHidden = true)] + public int Hidden { get; set; } +} - public int Bar { get; set; } - } +[GeneratedParser] +partial class NameTransformArguments +{ + [CommandLineArgument(Position = 0, IsRequired = true)] + public string testArg { get; set; } - class MethodArguments - { - // Using method arguments to store stuff in static fields isn't really recommended. It's - // done here for testing purposes only. - public static string CalledMethodName; - public static int Value; + [CommandLineArgument] + public int TestArg2 { get; set; } - [CommandLineArgument] - public static bool NoCancel() - { - CalledMethodName = nameof(NoCancel); - return true; - } + [CommandLineArgument] + public int __test__arg3__ { get; set; } - [CommandLineArgument] - public static bool Cancel() - { - CalledMethodName = nameof(Cancel); - return false; - } + [CommandLineArgument("ExplicitName")] + public int Explicit { get; set; } +} - [CommandLineArgument] - public static bool CancelWithHelp(CommandLineParser parser) - { - CalledMethodName = nameof(CancelWithHelp); - parser.HelpRequested = true; - return false; - } +[GeneratedParser] +partial class ValueDescriptionTransformArguments +{ + [CommandLineArgument] + public FileInfo Arg1 { get; set; } - [CommandLineArgument] - public static bool CancelWithValue(int value) - { - CalledMethodName = nameof(CancelWithValue); - Value = value; - return value > 0; - } + [CommandLineArgument] + public int Arg2 { get; set; } +} - [CommandLineArgument] - public static bool CancelWithValueAndHelp(int value, CommandLineParser parser) - { - CalledMethodName = nameof(CancelWithValueAndHelp); - Value = value; - // This should be reset to false if parsing continues. - parser.HelpRequested = true; - return value > 0; - } +[GeneratedParser] +partial class ValidationArguments +{ + public static int Arg3Value { get; set; } + + [CommandLineArgument] + [Description("Arg1 description.")] + [ValidateRange(1, 5)] + public int? Arg1 { get; set; } + + [CommandLineArgument("arg2", Position = 0)] + [ValidateNotEmpty, Description("Arg2 description.")] + public string Arg2 { get; set; } + + [CommandLineArgument] + [Description("Arg3 description.")] + [ValidatePattern("^[0-7]{4}$")] + [ValidateRange(1000, 7000)] + public static void Arg3(int value) + { + Arg3Value = value; + } - [CommandLineArgument] - public static void NoReturn() - { - CalledMethodName = nameof(NoReturn); - } + [CommandLineArgument] + [Description("Arg4 description.")] + [MultiValueSeparator(";")] + [ValidateStringLength(1, 3)] + [ValidateCount(2, 4)] + public string[] Arg4 { get; set; } + + [CommandLineArgument] + [Description("Day description.")] + [ValidateEnumValue] + public DayOfWeek Day { get; set; } + + [CommandLineArgument] + [Description("Day2 description.")] + [ValidateEnumValue] + public DayOfWeek? Day2 { get; set; } + + [CommandLineArgument] + [Description("NotNull description.")] + [ValidateNotNull] + public int? NotNull { get; set; } +} - [CommandLineArgument(Position = 0)] - public static void Positional(int value) - { - CalledMethodName = nameof(Positional); - Value = value; - } +[GeneratedParser] +// N.B. nameof is only safe if the argument name matches the property name. +[RequiresAny(nameof(Address), nameof(Path))] +partial class DependencyArguments +{ + [CommandLineArgument] + [Description("The address.")] + public IPAddress Address { get; set; } + + [CommandLineArgument(DefaultValue = (short)5000)] + [Description("The port.")] + [Requires(nameof(Address))] + public short Port { get; set; } + + [CommandLineArgument] + [Description("The throughput.")] + public int Throughput { get; set; } + + [CommandLineArgument] + [Description("The protocol.")] + [Requires(nameof(Address), nameof(Throughput))] + public int Protocol { get; set; } + + [CommandLineArgument] + [Description("The path.")] + [Prohibits("Address")] + public FileInfo Path { get; set; } +} - [CommandLineArgument] - public void NotStatic() - { - } +[GeneratedParser] +partial class MultiValueWhiteSpaceArguments +{ - [CommandLineArgument] - private static void NotPublic() - { - } + [CommandLineArgument(Position = 0)] + public int Arg1 { get; set; } - public static void NotAnArgument() - { - } - } + [CommandLineArgument(Position = 1)] + public int Arg2 { get; set; } - class AutomaticConflictingNameArguments - { - [CommandLineArgument] - public int Help { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public int[] Multi { get; set; } - [CommandLineArgument] - public int Version { get; set; } - } + [CommandLineArgument] + [MultiValueSeparator] + public int Other { get; set; } - [ParseOptions(Mode = ParsingMode.LongShort)] - class AutomaticConflictingShortNameArguments - { - [CommandLineArgument(ShortName = '?')] - public int Foo { get; set; } - } - class HiddenArguments - { - [CommandLineArgument] - public int Foo { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public bool[] MultiSwitch { get; set; } +} - [CommandLineArgument(IsHidden = true)] - public int Hidden { get; set; } - } +[GeneratedParser] +partial class InjectionArguments +{ + private readonly CommandLineParser _parser; - class NameTransformArguments + public InjectionArguments(CommandLineParser parser) { - public NameTransformArguments(string testArg) - { - } - - [CommandLineArgument] - public int TestArg2 { get; set; } - - [CommandLineArgument] - public int __test__arg3__ { get; set; } - - [CommandLineArgument("ExplicitName")] - public int Explicit { get; set; } + _parser = parser; } - class ValueDescriptionTransformArguments - { - [CommandLineArgument] - public FileInfo Arg1 { get; set; } + public CommandLineParser Parser => _parser; - [CommandLineArgument] - public int Arg2 { get; set; } - } + [CommandLineArgument] + public int Arg { get; set; } +} - class ValidationArguments - { - public static int Arg3Value { get; set; } +struct StructWithParseCulture +{ + public int Value { get; set; } - public ValidationArguments([ValidateNotEmpty, Description("Arg2 description.")] string arg2 = null) + public static StructWithParseCulture Parse(string value, IFormatProvider provider) + { + return new StructWithParseCulture() { - Arg2 = arg2; - } - - [CommandLineArgument] - [Description("Arg1 description.")] - [ValidateRange(1, 5)] - public int? Arg1 { get; set; } + Value = int.Parse(value, provider) + }; + } +} - public string Arg2 { get; set; } +struct StructWithParse +{ + public int Value { get; set; } - [CommandLineArgument] - [Description("Arg3 description.")] - [ValidatePattern("^[0-7]{4}$")] - [ValidateRange(1000, 7000)] - public static void Arg3(int value) + public static StructWithParse Parse(string value) + { + return new StructWithParse() { - Arg3Value = value; - } - - [CommandLineArgument] - [Description("Arg4 description.")] - [MultiValueSeparator(";")] - [ValidateStringLength(1, 3)] - [ValidateCount(2, 4)] - public string[] Arg4 { get; set; } - - [CommandLineArgument] - [Description("Day description.")] - [ValidateEnumValue] - public DayOfWeek Day { get; set; } - - [CommandLineArgument] - [Description("Day2 description.")] - [ValidateEnumValue] - public DayOfWeek? Day2 { get; set; } - - [CommandLineArgument] - [Description("NotNull description.")] - [ValidateNotNull] - public int? NotNull { get; set; } + Value = int.Parse(value, CultureInfo.InvariantCulture) + }; } +} - // N.B. nameof is only safe if the argument name matches the property name. - [RequiresAny(nameof(Address), nameof(Path))] - class DependencyArguments +struct StructWithCtor +{ + public StructWithCtor(string value) { - [CommandLineArgument] - [Description("The address.")] - public IPAddress Address { get; set; } - - [CommandLineArgument(DefaultValue = (short)5000)] - [Description("The port.")] - [Requires(nameof(Address))] - public short Port { get; set; } - - [CommandLineArgument] - [Description("The throughput.")] - public int Throughput { get; set; } - - [CommandLineArgument] - [Description("The protocol.")] - [Requires(nameof(Address), nameof(Throughput))] - public int Protocol { get; set; } - - [CommandLineArgument] - [Description("The path.")] - [Prohibits("Address")] - public FileInfo Path { get; set; } + Value = int.Parse(value); } - class MultiValueWhiteSpaceArguments - { + public int Value { get; set; } +} - [CommandLineArgument(Position = 0)] - public int Arg1 { get; set; } +[GeneratedParser] +partial class ConversionArguments +{ + [CommandLineArgument] + public StructWithParseCulture ParseCulture { get; set; } - [CommandLineArgument(Position = 1)] - public int Arg2 { get; set; } + [CommandLineArgument] + public StructWithParse ParseStruct { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public int[] Multi { get; set; } + [CommandLineArgument] + public StructWithCtor Ctor { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public int Other { get; set; } + [CommandLineArgument] + public StructWithParse? ParseNullable { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public StructWithParse[] ParseMulti { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public bool[] MultiSwitch { get; set; } - } + [CommandLineArgument] + [MultiValueSeparator] + public StructWithParse?[] ParseNullableMulti { get; set; } - class InjectionArguments - { - private readonly CommandLineParser _parser; + [CommandLineArgument] + [MultiValueSeparator] + public int?[] NullableMulti { get; set; } - public InjectionArguments(CommandLineParser parser) - { - _parser = parser; - } + [CommandLineArgument] + public int? Nullable { get; set; } +} - public CommandLineParser Parser => _parser; +[Description("Base class attribute.")] +class BaseArguments +{ + [CommandLineArgument] + public string BaseArg { get; set; } +} - [CommandLineArgument] - public int Arg { get; set; } - } +[GeneratedParser] +partial class DerivedArguments : BaseArguments +{ + [CommandLineArgument] + public int DerivedArg { get; set; } +} - class InjectionMixedArguments - { - private readonly CommandLineParser _parser; - private readonly int _arg1; - private readonly int _arg2; +[GeneratedParser] +partial class InitializerDefaultValueArguments +{ + [CommandLineArgument] + public string Arg1 { get; set; } = "foo\tbar\""; - public InjectionMixedArguments(int arg1, CommandLineParser parser, int arg2) - { - _arg1 = arg1; - _parser = parser; - _arg2 = arg2; - } + [CommandLineArgument] + public float Arg2 { get; set; } = 5.5f; - public CommandLineParser Parser => _parser; + [CommandLineArgument] + public int Arg3 { get; set; } = int.MaxValue; - public int Arg1 => _arg1; + [CommandLineArgument] + public DayOfWeek Arg4 { get; set; } = DayOfWeek.Tuesday; - public int Arg2 => _arg2; + [CommandLineArgument] + public int Arg5 { get; set; } = Value; - [CommandLineArgument] - public int Arg3 { get; set; } - } + [CommandLineArgument] + public int Arg6 { get; set; } = GetValue(); - struct StructWithParseCulture - { - public int Value { get; set; } + [CommandLineArgument] + public int Arg7 { get; set; } = default; - public static StructWithParseCulture Parse(string value, IFormatProvider provider) - { - return new StructWithParseCulture() - { - Value = int.Parse(value, provider) - }; - } - } +#nullable enable + [CommandLineArgument] + public string? Arg8 { get; set; } = default!; - struct StructWithParse - { - public int Value { get; set; } + [CommandLineArgument] + public string? Arg9 { get; set; } = null!; +#nullable disable - public static StructWithParse Parse(string value) - { - return new StructWithParse() - { - Value = int.Parse(value, CultureInfo.InvariantCulture) - }; - } - } + [CommandLineArgument(IncludeDefaultInUsageHelp = false)] + public int Arg10 { get; set; } = 10; - struct StructWithCtor - { - public StructWithCtor(string value) - { - Value = int.Parse(value); - } + private const int Value = 47; - public int Value { get; set; } - } + public static int GetValue() => 42; - class ConversionArguments - { - [CommandLineArgument] - public StructWithParseCulture ParseCulture { get; set; } +} + +[GeneratedParser] +partial class AutoPrefixAliasesArguments +{ + [CommandLineArgument(IsShort = true)] + public string Protocol { get; set; } - [CommandLineArgument] - public StructWithParse Parse { get; set; } + [CommandLineArgument] + public int Port { get; set; } - [CommandLineArgument] - public StructWithCtor Ctor { get; set; } + [CommandLineArgument(IsShort = true)] + [Alias("Prefix")] + public bool EnablePrefix { get; set; } +} + +class AutoPositionArgumentsBase +{ + [CommandLineArgument(IsPositional = true, IsRequired = true)] + public string BaseArg1 { get; set; } - [CommandLineArgument] - public StructWithParse? ParseNullable { get; set; } + [CommandLineArgument(IsPositional = true)] + public int BaseArg2 { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public StructWithParse[] ParseMulti { get; set; } + [CommandLineArgument] + public int BaseArg3 { get; set; } +} - [CommandLineArgument] - [MultiValueSeparator] - public StructWithParse?[] ParseNullableMulti { get; set; } +[GeneratedParser] +partial class AutoPositionArguments : AutoPositionArgumentsBase +{ + [CommandLineArgument(IsPositional = true)] + public string Arg1 { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public int?[] NullableMulti { get; set; } + [CommandLineArgument(IsPositional = true)] + public int Arg2 { get; set; } - [CommandLineArgument] - public int? Nullable { get; set; } - } + [CommandLineArgument] + public int Arg3 { get; set; } } diff --git a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs index 582583b6..603c0ad6 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs @@ -3,143 +3,152 @@ using System; using System.Text.RegularExpressions; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +/// +/// Independent tests of argument validators without having to go through parsing. +/// +[TestClass] +public class ArgumentValidatorTest { - /// - /// Independent tests of argument validators without having to go through parsing. - /// - [TestClass] - public class ArgumentValidatorTest + [TestMethod] + public void TestValidateRange() + { + var argument = GetArgument(); + var validator = new ValidateRangeAttribute(0, 10); + Assert.IsTrue(validator.IsValid(argument, 0)); + Assert.IsTrue(validator.IsValid(argument, 5)); + Assert.IsTrue(validator.IsValid(argument, 10)); + Assert.IsFalse(validator.IsValid(argument, -1)); + Assert.IsFalse(validator.IsValid(argument, 11)); + Assert.IsFalse(validator.IsValid(argument, null)); + + validator = new ValidateRangeAttribute(null, 10); + Assert.IsTrue(validator.IsValid(argument, 0)); + Assert.IsTrue(validator.IsValid(argument, 5)); + Assert.IsTrue(validator.IsValid(argument, 10)); + Assert.IsTrue(validator.IsValid(argument, int.MinValue)); + Assert.IsFalse(validator.IsValid(argument, 11)); + Assert.IsTrue(validator.IsValid(argument, null)); + + validator = new ValidateRangeAttribute(10, null); + Assert.IsTrue(validator.IsValid(argument, 10)); + Assert.IsTrue(validator.IsValid(argument, int.MaxValue)); + Assert.IsFalse(validator.IsValid(argument, 9)); + Assert.IsFalse(validator.IsValid(argument, null)); + } + + [TestMethod] + public void TestValidateNotNull() + { + var argument = GetArgument(); + var validator = new ValidateNotNullAttribute(); + Assert.IsTrue(validator.IsValid(argument, 1)); + Assert.IsTrue(validator.IsValid(argument, "hello")); + Assert.IsFalse(validator.IsValid(argument, null)); + } + + [TestMethod] + public void TestValidateNotNullOrEmpty() + { + var argument = GetArgument(); + var validator = new ValidateNotEmptyAttribute(); + Assert.IsTrue(validator.IsValid(argument, "hello")); + Assert.IsTrue(validator.IsValid(argument, " ")); + Assert.IsFalse(validator.IsValid(argument, null)); + Assert.IsFalse(validator.IsValid(argument, "")); + } + + [TestMethod] + public void TestValidateNotNullOrWhiteSpace() + { + var argument = GetArgument(); + var validator = new ValidateNotWhiteSpaceAttribute(); + Assert.IsTrue(validator.IsValid(argument, "hello")); + Assert.IsFalse(validator.IsValid(argument, " ")); + Assert.IsFalse(validator.IsValid(argument, null)); + Assert.IsFalse(validator.IsValid(argument, "")); + } + + [TestMethod] + public void TestValidateStringLength() + { + var argument = GetArgument(); + var validator = new ValidateStringLengthAttribute(2, 5); + Assert.IsTrue(validator.IsValid(argument, "ab")); + Assert.IsTrue(validator.IsValid(argument, "abcde")); + Assert.IsFalse(validator.IsValid(argument, "a")); + Assert.IsFalse(validator.IsValid(argument, "abcdef")); + Assert.IsFalse(validator.IsValid(argument, "")); + Assert.IsFalse(validator.IsValid(argument, null)); + + validator = new ValidateStringLengthAttribute(0, 5); + Assert.IsTrue(validator.IsValid(argument, "")); + Assert.IsTrue(validator.IsValid(argument, null)); + } + + [TestMethod] + public void ValidatePatternAttribute() + { + var argument = GetArgument(); + + // Partial match. + var validator = new ValidatePatternAttribute("[a-z]+"); + Assert.IsTrue(validator.IsValid(argument, "abc")); + Assert.IsTrue(validator.IsValid(argument, "0cde2")); + Assert.IsFalse(validator.IsValid(argument, "02")); + Assert.IsFalse(validator.IsValid(argument, "ABCD")); + Assert.IsFalse(validator.IsValid(argument, "")); + Assert.IsFalse(validator.IsValid(argument, null)); + + // Exact match. + validator = new ValidatePatternAttribute("^[a-z]+$"); + Assert.IsTrue(validator.IsValid(argument, "abc")); + Assert.IsFalse(validator.IsValid(argument, "0cde2")); + Assert.IsFalse(validator.IsValid(argument, "02")); + Assert.IsFalse(validator.IsValid(argument, "ABCD")); + Assert.IsFalse(validator.IsValid(argument, "")); + Assert.IsFalse(validator.IsValid(argument, null)); + + // Options + validator = new ValidatePatternAttribute("^[a-z]+$", RegexOptions.IgnoreCase); + Assert.IsTrue(validator.IsValid(argument, "abc")); + Assert.IsFalse(validator.IsValid(argument, "0cde2")); + Assert.IsFalse(validator.IsValid(argument, "02")); + Assert.IsTrue(validator.IsValid(argument, "ABCD")); + Assert.IsFalse(validator.IsValid(argument, "")); + Assert.IsFalse(validator.IsValid(argument, null)); + + Assert.AreEqual("The value for the argument 'Arg3' is not valid.", validator.GetErrorMessage(argument, "foo")); + validator.ErrorMessage = "Name {0}, value {1}, pattern {2}"; + Assert.AreEqual("Name Arg3, value foo, pattern ^[a-z]+$", validator.GetErrorMessage(argument, "foo")); + } + + [TestMethod] + public void TestValidateEnumValue() + { + var parser = new CommandLineParser(); + var validator = new ValidateEnumValueAttribute(); + var argument = parser.GetArgument("Day")!; + Assert.IsTrue(validator.IsValid(argument, DayOfWeek.Sunday)); + Assert.IsTrue(validator.IsValid(argument, DayOfWeek.Saturday)); + Assert.IsTrue(validator.IsValid(argument, null)); + Assert.IsFalse(validator.IsValid(argument, (DayOfWeek)9)); + + argument = parser.GetArgument("Day2")!; + Assert.IsTrue(validator.IsValid(argument, (DayOfWeek?)DayOfWeek.Sunday)); + Assert.IsTrue(validator.IsValid(argument, (DayOfWeek?)DayOfWeek.Saturday)); + Assert.IsTrue(validator.IsValid(argument, null)); + Assert.IsFalse(validator.IsValid(argument, (DayOfWeek?)9)); + } + + private static CommandLineArgument GetArgument() { - CommandLineParser _parser; - CommandLineArgument _argument; - - [TestInitialize] - public void Initialize() - { - // Just so we have a CommandLineArgument instance to pass. None of the built-in - // validators use that for anything other than the name and type. - _parser = new CommandLineParser(); - _argument = _parser.GetArgument("Arg3"); - } - - [TestMethod] - public void TestValidateRange() - { - var validator = new ValidateRangeAttribute(0, 10); - Assert.IsTrue(validator.IsValid(_argument, 0)); - Assert.IsTrue(validator.IsValid(_argument, 5)); - Assert.IsTrue(validator.IsValid(_argument, 10)); - Assert.IsFalse(validator.IsValid(_argument, -1)); - Assert.IsFalse(validator.IsValid(_argument, 11)); - Assert.IsFalse(validator.IsValid(_argument, null)); - - validator = new ValidateRangeAttribute(null, 10); - Assert.IsTrue(validator.IsValid(_argument, 0)); - Assert.IsTrue(validator.IsValid(_argument, 5)); - Assert.IsTrue(validator.IsValid(_argument, 10)); - Assert.IsTrue(validator.IsValid(_argument, int.MinValue)); - Assert.IsFalse(validator.IsValid(_argument, 11)); - Assert.IsTrue(validator.IsValid(_argument, null)); - - validator = new ValidateRangeAttribute(10, null); - Assert.IsTrue(validator.IsValid(_argument, 10)); - Assert.IsTrue(validator.IsValid(_argument, int.MaxValue)); - Assert.IsFalse(validator.IsValid(_argument, 9)); - Assert.IsFalse(validator.IsValid(_argument, null)); - } - - [TestMethod] - public void TestValidateNotNull() - { - var validator = new ValidateNotNullAttribute(); - Assert.IsTrue(validator.IsValid(_argument, 1)); - Assert.IsTrue(validator.IsValid(_argument, "hello")); - Assert.IsFalse(validator.IsValid(_argument, null)); - } - - [TestMethod] - public void TestValidateNotNullOrEmpty() - { - var validator = new ValidateNotEmptyAttribute(); - Assert.IsTrue(validator.IsValid(_argument, "hello")); - Assert.IsTrue(validator.IsValid(_argument, " ")); - Assert.IsFalse(validator.IsValid(_argument, null)); - Assert.IsFalse(validator.IsValid(_argument, "")); - } - - [TestMethod] - public void TestValidateNotNullOrWhiteSpace() - { - var validator = new ValidateNotWhiteSpaceAttribute(); - Assert.IsTrue(validator.IsValid(_argument, "hello")); - Assert.IsFalse(validator.IsValid(_argument, " ")); - Assert.IsFalse(validator.IsValid(_argument, null)); - Assert.IsFalse(validator.IsValid(_argument, "")); - } - - [TestMethod] - public void TestValidateStringLength() - { - var validator = new ValidateStringLengthAttribute(2, 5); - Assert.IsTrue(validator.IsValid(_argument, "ab")); - Assert.IsTrue(validator.IsValid(_argument, "abcde")); - Assert.IsFalse(validator.IsValid(_argument, "a")); - Assert.IsFalse(validator.IsValid(_argument, "abcdef")); - Assert.IsFalse(validator.IsValid(_argument, "")); - Assert.IsFalse(validator.IsValid(_argument, null)); - - validator = new ValidateStringLengthAttribute(0, 5); - Assert.IsTrue(validator.IsValid(_argument, "")); - Assert.IsTrue(validator.IsValid(_argument, null)); - } - - [TestMethod] - public void ValidatePatternAttribute() - { - // Partial match. - var validator = new ValidatePatternAttribute("[a-z]+"); - Assert.IsTrue(validator.IsValid(_argument, "abc")); - Assert.IsTrue(validator.IsValid(_argument, "0cde2")); - Assert.IsFalse(validator.IsValid(_argument, "02")); - Assert.IsFalse(validator.IsValid(_argument, "ABCD")); - Assert.IsFalse(validator.IsValid(_argument, "")); - Assert.IsFalse(validator.IsValid(_argument, null)); - - // Exact match. - validator = new ValidatePatternAttribute("^[a-z]+$"); - Assert.IsTrue(validator.IsValid(_argument, "abc")); - Assert.IsFalse(validator.IsValid(_argument, "0cde2")); - Assert.IsFalse(validator.IsValid(_argument, "02")); - Assert.IsFalse(validator.IsValid(_argument, "ABCD")); - Assert.IsFalse(validator.IsValid(_argument, "")); - Assert.IsFalse(validator.IsValid(_argument, null)); - - // Options - validator = new ValidatePatternAttribute("^[a-z]+$", RegexOptions.IgnoreCase); - Assert.IsTrue(validator.IsValid(_argument, "abc")); - Assert.IsFalse(validator.IsValid(_argument, "0cde2")); - Assert.IsFalse(validator.IsValid(_argument, "02")); - Assert.IsTrue(validator.IsValid(_argument, "ABCD")); - Assert.IsFalse(validator.IsValid(_argument, "")); - Assert.IsFalse(validator.IsValid(_argument, null)); - } - - [TestMethod] - public void TestValidateEnumValue() - { - var validator = new ValidateEnumValueAttribute(); - var argument = _parser.GetArgument("Day"); - Assert.IsTrue(validator.IsValid(argument, DayOfWeek.Sunday)); - Assert.IsTrue(validator.IsValid(argument, DayOfWeek.Saturday)); - Assert.IsTrue(validator.IsValid(argument, null)); - Assert.IsFalse(validator.IsValid(argument, (DayOfWeek)9)); - - argument = _parser.GetArgument("Day2"); - Assert.IsTrue(validator.IsValid(argument, (DayOfWeek?)DayOfWeek.Sunday)); - Assert.IsTrue(validator.IsValid(argument, (DayOfWeek?)DayOfWeek.Saturday)); - Assert.IsTrue(validator.IsValid(argument, null)); - Assert.IsFalse(validator.IsValid(argument, (DayOfWeek?)9)); - } + // Just so we have a CommandLineArgument instance to pass. None of the built-in + // validators use that for anything other than the name and type. + var parser = ValidationArguments.CreateParser(); + var arg = parser.GetArgument("Arg3"); + Assert.IsNotNull(arg); + return arg; } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 821a7482..90319438 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -1,340 +1,191 @@ -// Copyright (c) Sven Groot (Ookii.org) - -// These tests don't apply to .Net Framework. +// These tests don't apply to .Net Standard. #if NET6_0_OR_GREATER -#nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; +using Ookii.CommandLine.Support; using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; +using System.Reflection; + +namespace Ookii.CommandLine.Tests; -namespace Ookii.CommandLine.Tests +[TestClass] +public class CommandLineParserNullableTest { - [TestClass] - public class CommandLineParserNullableTest + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) { -#region Nested types - - class NullReturningStringConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - - return base.CanConvertFrom(context, sourceType); - } - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - if (value is string s) - { - if (s == "(null)") - { - return null; - } - else - { - return s; - } - } - - return base.ConvertFrom(context, culture, value); - } - } - - class NullReturningIntConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - - return base.CanConvertFrom(context, sourceType); - } - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - if (value is string s) - { - if (s == "(null)") - { - return null; - } - else - { - return int.Parse(s); - } - } - - return base.ConvertFrom(context, culture, value); - } - } - - class TestArguments - { - public TestArguments( - [TypeConverter(typeof(NullReturningStringConverter))] string? constructorNullable, - [TypeConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, - [TypeConverter(typeof(NullReturningIntConverter))] int constructorValueType, - [TypeConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) - { - ConstructorNullable = constructorNullable; - ConstructorNonNullable = constructorNonNullable; - ConstructorValueType = constructorValueType; - ConstructorNullableValueType = constructorNullableValueType; - } - - public string? ConstructorNullable { get; set; } - public string ConstructorNonNullable { get; set; } - public int ConstructorValueType { get; set; } - public int? ConstructorNullableValueType { get; set; } - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public string? Nullable { get; set; } = "NotNullDefaultValue"; - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public string NonNullable { get; set; } = string.Empty; - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] - public int ValueType { get; set; } - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] - public int? NullableValueType { get; set; } = 42; - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public string[]? NonNullableArray { get; set; } - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] - public int[]? ValueArray { get; set; } - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public ICollection NonNullableCollection { get; } = new List(); - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] - [MultiValueSeparator(";")] - public ICollection ValueCollection { get; } = new List(); - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public string?[]? NullableArray { get; set; } - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] - public string?[]? NullableValueArray { get; set; } - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public ICollection NullableCollection { get; } = new List(); - - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public ICollection NullableValueCollection { get; } = new List(); - - [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningStringConverter))] - public Dictionary? NonNullableDictionary { get; set; } - - [CommandLineArgument] - [ValueTypeConverter(typeof(NullReturningIntConverter))] - public Dictionary? ValueDictionary { get; set; } - - [CommandLineArgument] - [ValueTypeConverter(typeof(NullReturningStringConverter))] - public IDictionary NonNullableIDictionary { get; } = new Dictionary(); - - [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningIntConverter))] - [MultiValueSeparator(";")] - public IDictionary ValueIDictionary { get; } = new Dictionary(); - - [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningStringConverter))] - public Dictionary? NullableDictionary { get; set; } - - [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningIntConverter))] - public Dictionary? NullableValueDictionary { get; set; } - - [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningStringConverter))] - public IDictionary NullableIDictionary { get; } = new Dictionary(); - - [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningIntConverter))] - [MultiValueSeparator(";")] - public IDictionary NullableValueIDictionary { get; } = new Dictionary(); + // Get test coverage of reflection provider even on types that have the + // GeneratedParserAttribute. + ParseOptions.ForceReflectionDefault = true; + } - // This is an incorrect type converter (doesn't return KeyValuePair), but it doesn't - // matter since it'll only be used to test null values. - [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] - public Dictionary? InvalidDictionary { get; set; } - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAllowNull(ProviderKind kind) + { + var parser = CommandLineParserTest.CreateParser(kind); + Assert.IsTrue(parser.GetArgument("constructorNullable")!.AllowNull); + Assert.IsFalse(parser.GetArgument("constructorNonNullable")!.AllowNull); + Assert.IsFalse(parser.GetArgument("constructorValueType")!.AllowNull); + Assert.IsTrue(parser.GetArgument("constructorNullableValueType")!.AllowNull); + + Assert.IsTrue(parser.GetArgument("Nullable")!.AllowNull); + Assert.IsFalse(parser.GetArgument("NonNullable")!.AllowNull); + Assert.IsFalse(parser.GetArgument("ValueType")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableValueType")!.AllowNull); + + Assert.IsFalse(parser.GetArgument("NonNullableArray")!.AllowNull); + Assert.IsFalse(parser.GetArgument("ValueArray")!.AllowNull); + Assert.IsFalse(parser.GetArgument("NonNullableCollection")!.AllowNull); + Assert.IsFalse(parser.GetArgument("ValueCollection")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableArray")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableValueArray")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableCollection")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableValueCollection")!.AllowNull); + + Assert.IsFalse(parser.GetArgument("NonNullableDictionary")!.AllowNull); + Assert.IsFalse(parser.GetArgument("ValueDictionary")!.AllowNull); + Assert.IsFalse(parser.GetArgument("NonNullableIDictionary")!.AllowNull); + Assert.IsFalse(parser.GetArgument("ValueIDictionary")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableDictionary")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableValueDictionary")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableIDictionary")!.AllowNull); + Assert.IsTrue(parser.GetArgument("NullableValueIDictionary")!.AllowNull); + } -#endregion + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableProperties(ProviderKind kind) + { + var parser = CommandLineParserTest.CreateParser(kind); + ExpectNullException(parser, "NonNullable", "foo", "bar", "4", "5", "-NonNullable", "(null)"); + ExpectNullException(parser, "ValueType", "foo", "bar", "4", "5", "-ValueType", "(null)"); + var result = ExpectSuccess(parser, "foo", "bar", "4", "5", "-NonNullable", "baz", "-ValueType", "47", "-Nullable", "(null)", "-NullableValueType", "(null)"); + Assert.IsNull(result.Nullable); + Assert.AreEqual("baz", result.NonNullable); + Assert.AreEqual(47, result.ValueType); + Assert.IsNull(result.NullableValueType); + } - [TestMethod] - public void TestAllowNull() - { - var parser = new CommandLineParser(typeof(TestArguments)); - Assert.IsTrue(parser.GetArgument("constructorNullable")!.AllowNull); - Assert.IsFalse(parser.GetArgument("constructorNonNullable")!.AllowNull); - Assert.IsFalse(parser.GetArgument("constructorValueType")!.AllowNull); - Assert.IsTrue(parser.GetArgument("constructorNullableValueType")!.AllowNull); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableMultiValue(ProviderKind kind) + { + var parser = CommandLineParserTest.CreateParser(kind); + ExpectNullException(parser, "NonNullableArray", "-NonNullableArray", "foo", "-NonNullableArray", "(null)"); + ExpectNullException(parser, "NonNullableCollection", "-NonNullableCollection", "foo", "-NonNullableCollection", "(null)"); + ExpectNullException(parser, "ValueArray", "-ValueArray", "5", "-ValueArray", "(null)"); + ExpectNullException(parser, "ValueCollection", "-ValueCollection", "5;(null)"); + ExpectNullException(parser, "ValueCollection", "-ValueCollection", "5", "-ValueCollection", "(null)"); + var result = ExpectSuccess(parser, "a", "b", "4", "5", "-NonNullableArray", "foo", "-NonNullableArray", "bar", + "-NonNullableCollection", "baz", "-NonNullableCollection", "bif", + "-ValueArray", "5", "-ValueArray", "6", + "-ValueCollection", "6;7", + "-NullableValueArray", "(null)", + "-NullableValueCollection", "(null)", + "-NullableArray", "(null)", + "-NullableCollection", "(null)" + ); + CollectionAssert.AreEqual(new[] { "foo", "bar", }, result.NonNullableArray); + CollectionAssert.AreEqual(new[] { "baz", "bif", }, (List)result.NonNullableCollection); + CollectionAssert.AreEqual(new[] { 5, 6 }, result.ValueArray); + CollectionAssert.AreEqual(new[] { 6, 7 }, (List)result.ValueCollection); + CollectionAssert.AreEqual(new int?[] { null }, result.NullableValueArray); + CollectionAssert.AreEqual(new int?[] { null }, (List)result.NullableValueCollection); + CollectionAssert.AreEqual(new string?[] { null }, (List)result.NullableCollection); + CollectionAssert.AreEqual(new string?[] { null }, result.NullableArray); + } - Assert.IsTrue(parser.GetArgument("Nullable")!.AllowNull); - Assert.IsFalse(parser.GetArgument("NonNullable")!.AllowNull); - Assert.IsFalse(parser.GetArgument("ValueType")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableValueType")!.AllowNull); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableDictionary(ProviderKind kind) + { + var parser = CommandLineParserTest.CreateParser(kind); + ExpectNullException(parser, "NonNullableDictionary", "-NonNullableDictionary", "foo=bar", "-NonNullableDictionary", "baz=(null)"); + ExpectNullException(parser, "NonNullableIDictionary", "-NonNullableIDictionary", "foo=bar", "-NonNullableIDictionary", "baz=(null)"); + ExpectNullException(parser, "ValueDictionary", "-ValueDictionary", "foo=5", "-ValueDictionary", "foo=(null)"); + ExpectNullException(parser, "ValueIDictionary", "-ValueIDictionary", "foo=5;bar=(null)"); + ExpectNullException(parser, "ValueIDictionary", "-ValueIDictionary", "foo=5", "-ValueIDictionary", "bar=(null)"); + // A null key is never allowed. + ExpectNullException(parser, "NullableDictionary", "-NullableDictionary", "(null)=foo"); + // The whole KeyValuePair being null is never allowed. + ExpectNullException(parser, "InvalidDictionary", "-InvalidDictionary", "(null)"); + var result = ExpectSuccess(parser, "a", "b", "4", "5", "-NonNullableDictionary", "foo=bar", "-NonNullableDictionary", "bar=baz", + "-NonNullableIDictionary", "baz=bam", "-NonNullableIDictionary", "bif=zap", + "-ValueDictionary", "foo=5", "-ValueDictionary", "bar=6", + "-ValueIDictionary", "foo=6;bar=7", + "-NullableValueDictionary", "foo=(null)", + "-NullableValueIDictionary", "bar=(null)", + "-NullableDictionary", "baz=(null)", + "-NullableIDictionary", "bif=(null)" + ); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", "bar"), KeyValuePair.Create("bar", "baz") }, result.NonNullableDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("baz", "bam"), KeyValuePair.Create("bif", "zap") }, (Dictionary)result.NonNullableIDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", 5), KeyValuePair.Create("bar", 6) }, result.ValueDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", 6), KeyValuePair.Create("bar", 7) }, (Dictionary)result.ValueIDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", (int?)null) }, result.NullableValueDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("bar", (int?)null) }, (Dictionary)result.NullableValueIDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("baz", (string?)null) }, result.NullableDictionary); + CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("bif", (string?)null) }, (Dictionary)result.NullableIDictionary); + } - Assert.IsFalse(parser.GetArgument("NonNullableArray")!.AllowNull); - Assert.IsFalse(parser.GetArgument("ValueArray")!.AllowNull); - Assert.IsFalse(parser.GetArgument("NonNullableCollection")!.AllowNull); - Assert.IsFalse(parser.GetArgument("ValueCollection")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableArray")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableValueArray")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableCollection")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableValueCollection")!.AllowNull); +#if NET7_0_OR_GREATER - Assert.IsFalse(parser.GetArgument("NonNullableDictionary")!.AllowNull); - Assert.IsFalse(parser.GetArgument("ValueDictionary")!.AllowNull); - Assert.IsFalse(parser.GetArgument("NonNullableIDictionary")!.AllowNull); - Assert.IsFalse(parser.GetArgument("ValueIDictionary")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableDictionary")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableValueDictionary")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableIDictionary")!.AllowNull); - Assert.IsTrue(parser.GetArgument("NullableValueIDictionary")!.AllowNull); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestRequiredProperty(ProviderKind kind) + { + var parser = CommandLineParserTest.CreateParser(kind); + Assert.IsTrue(parser.GetArgument("Arg1")!.IsRequired); + Assert.IsTrue(parser.GetArgument("Arg1")!.IsRequiredProperty); + Assert.IsFalse(parser.GetArgument("Arg1")!.AllowNull); + Assert.IsTrue(parser.GetArgument("Foo")!.IsRequired); + Assert.IsTrue(parser.GetArgument("Foo")!.IsRequiredProperty); + Assert.IsTrue(parser.GetArgument("Foo")!.AllowNull); + Assert.IsTrue(parser.GetArgument("Bar")!.IsRequired); + Assert.IsTrue(parser.GetArgument("Bar")!.IsRequiredProperty); + Assert.IsFalse(parser.GetArgument("Bar")!.AllowNull); + var result = ExpectSuccess(parser, "-Arg1", "test", "-Foo", "foo", "-Bar", "42"); + Assert.AreEqual("test", result.Arg1); + Assert.AreEqual("foo", result.Foo); + CollectionAssert.AreEqual(new[] { 42 }, result.Bar); + Assert.IsNull(result.Arg2); + } - [TestMethod] - public void TestNonNullableConstructor() - { - var parser = new CommandLineParser(typeof(TestArguments)); - ExpectNullException(parser, "constructorNonNullable", "foo", "(null)", "4", "5"); - ExpectNullException(parser, "constructorValueType", "foo", "bar", "(null)", "5"); - var result = ExpectSuccess(parser, "(null)", "bar", "4", "(null)"); - Assert.IsNull(result.ConstructorNullable); - Assert.AreEqual("bar", result.ConstructorNonNullable); - Assert.AreEqual(4, result.ConstructorValueType); - Assert.IsNull(result.ConstructorNullableValueType); - } +#endif - [TestMethod] - public void TestNonNullableProperties() + private static void ExpectNullException(CommandLineParser parser, string argumentName, params string[] args) + { + try { - var parser = new CommandLineParser(typeof(TestArguments)); - ExpectNullException(parser, "NonNullable", "foo", "bar", "4", "5", "-NonNullable", "(null)"); - ExpectNullException(parser, "ValueType", "foo", "bar", "4", "5", "-ValueType", "(null)"); - var result = ExpectSuccess(parser, "foo", "bar", "4", "5", "-NonNullable", "baz", "-ValueType", "47", "-Nullable", "(null)", "-NullableValueType", "(null)"); - Assert.IsNull(result.Nullable); - Assert.AreEqual("baz", result.NonNullable); - Assert.AreEqual(47, result.ValueType); - Assert.IsNull(result.NullableValueType); + parser.Parse(args); + Assert.Fail("Expected exception not thrown."); } - - [TestMethod] - public void TestNonNullableMultiValue() + catch (CommandLineArgumentException ex) { - var parser = new CommandLineParser(typeof(TestArguments)); - ExpectNullException(parser, "NonNullableArray", "-NonNullableArray", "foo", "-NonNullableArray", "(null)"); - ExpectNullException(parser, "NonNullableCollection", "-NonNullableCollection", "foo", "-NonNullableCollection", "(null)"); - ExpectNullException(parser, "ValueArray", "-ValueArray", "5", "-ValueArray", "(null)"); - ExpectNullException(parser, "ValueCollection", "-ValueCollection", "5;(null)"); - ExpectNullException(parser, "ValueCollection", "-ValueCollection", "5", "-ValueCollection", "(null)"); - var result = ExpectSuccess(parser, "a", "b", "4", "5", "-NonNullableArray", "foo", "-NonNullableArray", "bar", - "-NonNullableCollection", "baz", "-NonNullableCollection", "bif", - "-ValueArray", "5", "-ValueArray", "6", - "-ValueCollection", "6;7", - "-NullableValueArray", "(null)", - "-NullableValueCollection", "(null)", - "-NullableArray", "(null)", - "-NullableCollection", "(null)" - ); - CollectionAssert.AreEqual(new[] { "foo", "bar", }, result.NonNullableArray); - CollectionAssert.AreEqual(new[] { "baz", "bif", }, (List)result.NonNullableCollection); - CollectionAssert.AreEqual(new[] { 5, 6 }, result.ValueArray); - CollectionAssert.AreEqual(new[] { 6, 7 }, (List)result.ValueCollection); - CollectionAssert.AreEqual(new int?[] { null }, result.NullableValueArray); - CollectionAssert.AreEqual(new int?[] { null }, (List)result.NullableValueCollection); - CollectionAssert.AreEqual(new string?[] { null }, (List)result.NullableCollection); - CollectionAssert.AreEqual(new string?[] { null }, result.NullableArray); + Assert.AreEqual(CommandLineArgumentErrorCategory.NullArgumentValue, ex.Category); + Assert.AreEqual(argumentName, ex.ArgumentName); } + } - [TestMethod] - public void TestNonNullableDictionary() - { - var parser = new CommandLineParser(typeof(TestArguments)); - ExpectNullException(parser, "NonNullableDictionary", "-NonNullableDictionary", "foo=bar", "-NonNullableDictionary", "baz=(null)"); - ExpectNullException(parser, "NonNullableIDictionary", "-NonNullableIDictionary", "foo=bar", "-NonNullableIDictionary", "baz=(null)"); - ExpectNullException(parser, "ValueDictionary", "-ValueDictionary", "foo=5", "-ValueDictionary", "foo=(null)"); - ExpectNullException(parser, "ValueIDictionary", "-ValueIDictionary", "foo=5;bar=(null)"); - ExpectNullException(parser, "ValueIDictionary", "-ValueIDictionary", "foo=5", "-ValueIDictionary", "bar=(null)"); - // A null key is never allowed. - ExpectNullException(parser, "NullableDictionary", "-NullableDictionary", "(null)=foo"); - // The whole KeyValuePair being null is never allowed. - ExpectNullException(parser, "InvalidDictionary", "-InvalidDictionary", "(null)"); - var result = ExpectSuccess(parser, "a", "b", "4", "5", "-NonNullableDictionary", "foo=bar", "-NonNullableDictionary", "bar=baz", - "-NonNullableIDictionary", "baz=bam", "-NonNullableIDictionary", "bif=zap", - "-ValueDictionary", "foo=5", "-ValueDictionary", "bar=6", - "-ValueIDictionary", "foo=6;bar=7", - "-NullableValueDictionary", "foo=(null)", - "-NullableValueIDictionary", "bar=(null)", - "-NullableDictionary", "baz=(null)", - "-NullableIDictionary", "bif=(null)" - ); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", "bar"), KeyValuePair.Create("bar", "baz") }, result.NonNullableDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("baz", "bam"), KeyValuePair.Create("bif", "zap") }, (Dictionary)result.NonNullableIDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", 5), KeyValuePair.Create("bar", 6) }, result.ValueDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", 6), KeyValuePair.Create("bar", 7) }, (Dictionary)result.ValueIDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("foo", (int?)null) }, result.NullableValueDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("bar", (int?)null) }, (Dictionary)result.NullableValueIDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("baz", (string?)null) }, result.NullableDictionary); - CollectionAssert.AreEquivalent(new[] { KeyValuePair.Create("bif", (string?)null) }, (Dictionary)result.NullableIDictionary); + private static T ExpectSuccess(CommandLineParser parser, params string[] args) + where T : class + { + var result = parser.Parse(args); + Assert.IsNotNull(result); + return result; + } - } + public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) + => $"{methodInfo.Name} ({data[0]})"; - private static void ExpectNullException(CommandLineParser parser, string argumentName, params string[] args) - { - try - { - parser.Parse(args); - Assert.Fail("Expected exception not thrown."); - } - catch (CommandLineArgumentException ex) - { - Assert.AreEqual(CommandLineArgumentErrorCategory.NullArgumentValue, ex.Category); - Assert.AreEqual(argumentName, ex.ArgumentName); - } - } - private static TestArguments ExpectSuccess(CommandLineParser parser, params string[] args) + public static IEnumerable ProviderKinds + => new[] { - var result = (TestArguments?)parser.Parse(args); - Assert.IsNotNull(result); - return result; - } - } + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } + }; } #endif diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs index f7c1cf48..d540ec54 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs @@ -1,10 +1,10 @@ -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +partial class CommandLineParserTest { - public partial class CommandLineParserTest - { - private const string _executableName = "test"; + private const string _executableName = "test"; - private static readonly string _expectedDefaultUsage = @"Test arguments description. + private static readonly string _expectedDefaultUsage = @"Test arguments description. Usage: test [/arg1] [[/other] ] [[/notSwitch] ] [[/Arg5] ] [[/other2] ] [[/Arg8] ...] /Arg6 [/Arg10...] [/Arg11] [/Arg12 ...] [/Arg13 ...] [/Arg14 ...] [/Arg15 >] [/Arg3 ] [/Arg7] [/Arg9 ] [/Help] [/Version] @@ -43,7 +43,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedLongShortUsage = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] + private static readonly string _expectedLongShortUsage = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] -f, --foo Foo description. Default value: 0. @@ -74,7 +74,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedLongShortUsageShortNameSyntax = @"Usage: test [[-f] ] [[--bar] ] [[-a] ] [--Arg1 ] [-?] [-S] [-k] [-u] [--Version] + private static readonly string _expectedLongShortUsageShortNameSyntax = @"Usage: test [[-f] ] [[--bar] ] [[-a] ] [--Arg1 ] [-?] [-S] [-k] [-u] [--Version] -f, --foo Foo description. Default value: 0. @@ -105,7 +105,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedLongShortUsageAbbreviated = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [arguments] + private static readonly string _expectedLongShortUsageAbbreviated = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [arguments] -f, --foo Foo description. Default value: 0. @@ -136,7 +136,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageDescriptionOnly = @"Test arguments description. + private static readonly string _expectedUsageDescriptionOnly = @"Test arguments description. Usage: test [-arg1] [[-other] ] [[-notSwitch] ] [[-Arg5] ] [[-other2] ] [[-Arg8] ...] -Arg6 [-Arg10...] [-Arg11] [-Arg12 ...] [-Arg13 ...] [-Arg14 ...] [-Arg15 >] [-Arg3 ] [-Arg7] [-Arg9 ] [-Help] [-Version] @@ -163,7 +163,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAll = @"Test arguments description. + private static readonly string _expectedUsageAll = @"Test arguments description. Usage: test [-arg1] [[-other] ] [[-notSwitch] ] [[-Arg5] ] [[-other2] ] [[-Arg8] ...] -Arg6 [-Arg10...] [-Arg11] [-Arg12 ...] [-Arg13 ...] [-Arg14 ...] [-Arg15 >] [-Arg3 ] [-Arg7] [-Arg9 ] [-Help] [-Version] @@ -223,15 +223,15 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageNone = @"Test arguments description. + private static readonly string _expectedUsageNone = @"Test arguments description. Usage: test [-arg1] [[-other] ] [[-notSwitch] ] [[-Arg5] ] [[-other2] ] [[-Arg8] ...] -Arg6 [-Arg10...] [-Arg11] [-Arg12 ...] [-Arg13 ...] [-Arg14 ...] [-Arg15 >] [-Arg3 ] [-Arg7] [-Arg9 ] [-Help] [-Version] ".ReplaceLineEndings(); - // Raw strings would be nice here so including the escape character directly wouldn't be - // necessary but that requires C# 11. - private static readonly string _expectedUsageColor = @"Test arguments description. + // Raw strings would be nice here so including the escape character directly wouldn't be + // necessary but that requires C# 11. + private static readonly string _expectedUsageColor = @"Test arguments description. Usage: test [/arg1] [[/other] ] [[/notSwitch] ] [[/Arg5] ] [[/other2] ] [[/Arg8] ...] /Arg6 [/Arg10...] [/Arg11] [/Arg12 ...] [/Arg13 ...] [/Arg14 ...] [/Arg15 >] [/Arg3 ] [/Arg7] [/Arg9 ] [/Help] [/Version] @@ -270,7 +270,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedLongShortUsageColor = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] + private static readonly string _expectedLongShortUsageColor = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] -f, --foo  Foo description. Default value: 0. @@ -301,7 +301,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageHidden = @"Usage: test [-Foo ] [-Help] [-Version] + private static readonly string _expectedUsageHidden = @"Usage: test [-Foo ] [-Help] [-Version] -Foo @@ -314,7 +314,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageValidators = @"Usage: test [[-arg2] ] [-Arg1 ] [-Arg3 ] [-Arg4 ...] [-Day ] [-Day2 ] [-Help] [-NotNull ] [-Version] + private static readonly string _expectedUsageValidators = @"Usage: test [[-arg2] ] [-Arg1 ] [-Arg3 ] [-Arg4 ...] [-Day ] [-Day2 ] [-Help] [-NotNull ] [-Version] -arg2 Arg2 description. Must not be empty. @@ -345,7 +345,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageDependencies = @"Usage: test [-Address ] [-Help] [-Path ] [-Port ] [-Protocol ] [-Throughput ] [-Version] + private static readonly string _expectedUsageDependencies = @"Usage: test [-Address ] [-Help] [-Path ] [-Port ] [-Protocol ] [-Throughput ] [-Version] You must use at least one of: -Address, -Path. @@ -372,7 +372,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageDependenciesDisabled = @"Usage: test [-Address ] [-Help] [-Path ] [-Port ] [-Protocol ] [-Throughput ] [-Version] + private static readonly string _expectedUsageDependenciesDisabled = @"Usage: test [-Address ] [-Help] [-Path ] [-Port ] [-Protocol ] [-Throughput ] [-Version] -Address The address. @@ -397,7 +397,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAlphabeticalLongName = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] + private static readonly string _expectedUsageAlphabeticalLongName = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] --Arg1 Arg1 description. @@ -428,7 +428,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAlphabeticalLongNameDescending = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] + private static readonly string _expectedUsageAlphabeticalLongNameDescending = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] --Version [] Displays version information. @@ -459,7 +459,7 @@ Arg1 description. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAlphabeticalShortName = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] + private static readonly string _expectedUsageAlphabeticalShortName = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] -?, --Help [] (-h) Displays this help message. @@ -490,7 +490,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAlphabeticalShortNameDescending = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] + private static readonly string _expectedUsageAlphabeticalShortNameDescending = @"Usage: test [[--foo] ] [[--bar] ] [[--Arg2] ] [--Arg1 ] [--Help] [--Switch1] [--Switch2] [-u] [--Version] --Version [] Displays version information. @@ -521,7 +521,7 @@ Displays this help message. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAlphabetical = @"Usage: test [[-foo] ] [[-bar] ] [[-Arg2] ] [-Arg1 ] [-Help] [-Switch1] [-Switch2] [-Switch3] [-Version] + private static readonly string _expectedUsageAlphabetical = @"Usage: test [[-foo] ] [[-bar] ] [[-Arg2] ] [-Arg1 ] [-Help] [-Switch1] [-Switch2] [-Switch3] [-Version] -Arg1 Arg1 description. @@ -552,7 +552,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageAlphabeticalDescending = @"Usage: test [[-foo] ] [[-bar] ] [[-Arg2] ] [-Arg1 ] [-Help] [-Switch1] [-Switch2] [-Switch3] [-Version] + private static readonly string _expectedUsageAlphabeticalDescending = @"Usage: test [[-foo] ] [[-bar] ] [[-Arg2] ] [-Arg1 ] [-Help] [-Switch1] [-Switch2] [-Switch3] [-Version] -Version [] Displays version information. @@ -583,15 +583,15 @@ Arg1 description. ".ReplaceLineEndings(); - private static readonly string _expectedUsageSyntaxOnly = @"Usage: test [/arg1] [[/other] ] [[/notSwitch] ] [[/Arg5] ] [[/other2] ] [[/Arg8] ...] /Arg6 [/Arg10...] [/Arg11] [/Arg12 ...] [/Arg13 ...] [/Arg14 ...] [/Arg15 >] [/Arg3 ] [/Arg7] [/Arg9 ] [/Help] [/Version] + private static readonly string _expectedUsageSyntaxOnly = @"Usage: test [/arg1] [[/other] ] [[/notSwitch] ] [[/Arg5] ] [[/other2] ] [[/Arg8] ...] /Arg6 [/Arg10...] [/Arg11] [/Arg12 ...] [/Arg13 ...] [/Arg14 ...] [/Arg15 >] [/Arg3 ] [/Arg7] [/Arg9 ] [/Help] [/Version] Run 'test /Help' for more information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageMessageOnly = @"Run 'test /Help' for more information. + private static readonly string _expectedUsageMessageOnly = @"Run 'test /Help' for more information. ".ReplaceLineEndings(); - private static readonly string _expectedUsageSeparator = @"Test arguments description. + private static readonly string _expectedUsageSeparator = @"Test arguments description. Usage: test [/arg1:] [[/other:]] [[/notSwitch:]] [[/Arg5:]] [[/other2:]] [[/Arg8:]...] /Arg6: [/Arg10...] [/Arg11] [/Arg12:...] [/Arg13:...] [/Arg14:...] [/Arg15:>] [/Arg3:] [/Arg7] [/Arg9:] [/Help] [/Version] @@ -630,7 +630,7 @@ Displays version information. ".ReplaceLineEndings(); - private static readonly string _expectedCustomIndentUsage = @"Test arguments description. + private static readonly string _expectedCustomIndentUsage = @"Test arguments description. Usage: test [-arg1] [[-other] ] [[-notSwitch] ] [[-Arg5] ] [[-other2] ] [[-Arg8] ...] -Arg6 [-Arg10...] [-Arg11] [-Arg12 ...] [-Arg13 ...] [-Arg14 ...] [-Arg15 >] [-Arg3 ] [-Arg7] [-Arg9 ] [-Help] [-Version] @@ -668,5 +668,4 @@ Displays this help message. Displays version information. ".ReplaceLineEndings(); - } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 64f4a798..2a18c765 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1,1288 +1,1563 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Support; +using Ookii.CommandLine.Tests.Commands; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; using System.Reflection; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +/// +///This is a test class for CommandLineParserTest and is intended +///to contain all CommandLineParserTest Unit Tests +/// +[TestClass()] +public partial class CommandLineParserTest { - /// - ///This is a test class for CommandLineParserTest and is intended - ///to contain all CommandLineParserTest Unit Tests - /// - [TestClass()] - public partial class CommandLineParserTest + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) { -#if NET6_0_OR_GREATER - private static readonly Type ArgumentConversionInner = typeof(ArgumentException); -#else - // Number converters on .Net Framework throw Exception. It's not my fault. - private static readonly Type ArgumentConversionInner = typeof(Exception); -#endif + // Get test coverage of reflection provider even on types that have the + // GeneratedParserAttribute. + ParseOptions.ForceReflectionDefault = true; + } - /// - ///A test for CommandLineParser Constructor - /// - [TestMethod()] - public void ConstructorEmptyArgumentsTest() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ConstructorEmptyArgumentsTest(ProviderKind kind) + { + Type argumentsType = typeof(EmptyArguments); + var target = CreateParser(kind); + Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); + Assert.AreEqual(false, target.AllowDuplicateArguments); + Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); + Assert.AreEqual(ParsingMode.Default, target.Mode); + CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), target.ArgumentNamePrefixes); + Assert.IsNull(target.LongArgumentNamePrefix); + Assert.AreEqual(argumentsType, target.ArgumentsType); + Assert.AreEqual("Ookii.CommandLine Unit Tests", target.ApplicationFriendlyName); + Assert.AreEqual(string.Empty, target.Description); + Assert.AreEqual(2, target.Arguments.Length); + VerifyArguments(target.Arguments, new[] { - Type argumentsType = typeof(EmptyArguments); - CommandLineParser target = new CommandLineParser(argumentsType); - Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); - Assert.AreEqual(false, target.AllowDuplicateArguments); - Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); - Assert.AreEqual(ParsingMode.Default, target.Mode); - CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), target.ArgumentNamePrefixes); - Assert.IsNull(target.LongArgumentNamePrefix); - Assert.AreEqual(argumentsType, target.ArgumentsType); - Assert.AreEqual(Assembly.GetExecutingAssembly().GetName().Name, target.ApplicationFriendlyName); - Assert.AreEqual(string.Empty, target.Description); - Assert.AreEqual(2, target.Arguments.Count); - using var args = target.Arguments.GetEnumerator(); - TestArguments(target.Arguments, new[] - { - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, - new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); - } + new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, + new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, + }); + } - [TestMethod()] - public void ConstructorTest() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ConstructorTest(ProviderKind kind) + { + Type argumentsType = typeof(TestArguments); + var target = CreateParser(kind); + Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); + Assert.AreEqual(false, target.AllowDuplicateArguments); + Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); + Assert.AreEqual(ParsingMode.Default, target.Mode); + CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), target.ArgumentNamePrefixes); + Assert.IsNull(target.LongArgumentNamePrefix); + Assert.AreEqual(argumentsType, target.ArgumentsType); + Assert.AreEqual("Friendly name", target.ApplicationFriendlyName); + Assert.AreEqual("Test arguments description.", target.Description); + Assert.AreEqual(18, target.Arguments.Length); + VerifyArguments(target.Arguments, new[] { - Type argumentsType = typeof(TestArguments); - CommandLineParser target = new CommandLineParser(argumentsType); - Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); - Assert.AreEqual(false, target.AllowDuplicateArguments); - Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); - Assert.AreEqual(ParsingMode.Default, target.Mode); - CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), target.ArgumentNamePrefixes); - Assert.IsNull(target.LongArgumentNamePrefix); - Assert.AreEqual(argumentsType, target.ArgumentsType); - Assert.AreEqual("Friendly name", target.ApplicationFriendlyName); - Assert.AreEqual("Test arguments description.", target.Description); - Assert.AreEqual(18, target.Arguments.Count); - TestArguments(target.Arguments, new[] - { - new ExpectedArgument("arg1", typeof(string)) { 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)) { Position = 2, DefaultValue = false }, - new ExpectedArgument("Arg5", typeof(float)) { Position = 3, Description = "Arg5 description." }, - 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("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 }, - new ExpectedArgument("Arg13", typeof(Dictionary), ArgumentKind.Dictionary) { ElementType = typeof(KeyValuePair), ValueDescription = "String=Int32" }, - 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("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("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); - } + 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("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 }, + new ExpectedArgument("Arg13", typeof(Dictionary), ArgumentKind.Dictionary) { ElementType = typeof(KeyValuePair), ValueDescription = "String=Int32" }, + 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("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("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, + }); + } - [TestMethod] - public void ConstructorMultipleArgumentConstructorsTest() - { - Type argumentsType = typeof(MultipleConstructorsArguments); - CommandLineParser target = new CommandLineParser(argumentsType); - Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); - Assert.AreEqual(false, target.AllowDuplicateArguments); - Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); - Assert.AreEqual(ParsingMode.Default, target.Mode); - CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), target.ArgumentNamePrefixes); - Assert.IsNull(target.LongArgumentNamePrefix); - Assert.AreEqual(argumentsType, target.ArgumentsType); - Assert.AreEqual("", target.Description); - Assert.AreEqual(4, target.Arguments.Count); // Constructor argument + one property argument. - TestArguments(target.Arguments, new[] - { - new ExpectedArgument("arg1", typeof(string)) { Position = 0, IsRequired = true }, - new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, - new ExpectedArgument("ThrowingArgument", typeof(int)), - new ExpectedArgument("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); - } + [TestMethod] + public void TestConstructorGeneratedProvider() + { + // Modify the default instead of explicitly creating options to make sure that the default + // is correct. + ParseOptions.ForceReflectionDefault = false; + + // The constructor should find and use the generated provider. + var parser = new CommandLineParser(); + Assert.AreEqual(ProviderKind.Generated, parser.ProviderKind); + + // Change back for other tests. + ParseOptions.ForceReflectionDefault = true; + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTest(ProviderKind kind) + { + var target = CreateParser(kind); + // Only required arguments + TestParse(target, "val1 2 -arg6 val6", "val1", 2, arg6: "val6"); + // Make sure negative numbers are accepted, and not considered an argument name. + TestParse(target, "val1 -2 -arg6 val6", "val1", -2, arg6: "val6"); + // 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 }); + // 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 }); + // 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)); + // 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); + // Short name cannot be used + CheckThrows(target, new[] { "val1", "2", "-arg6", "val6", "-a:5.5" }, CommandLineArgumentErrorCategory.UnknownArgument, "a", remainingArgumentCount: 1); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + 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); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestTooManyArguments(ProviderKind kind) + { + var target = CreateParser(kind); + + // Only accepts one positional argument. + CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 1); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestPropertySetterThrows(ProviderKind kind) + { + var target = CreateParser(kind); + + // No remaining arguments; exception happens after parsing finishes. + CheckThrows(target, + new[] { "-ThrowingArgument", "-5" }, + CommandLineArgumentErrorCategory.ApplyValueError, + "ThrowingArgument", + typeof(ArgumentOutOfRangeException)); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestConstructorThrows(ProviderKind kind) + { + var target = CreateParser(kind); + + CheckThrows(target, + Array.Empty(), + CommandLineArgumentErrorCategory.CreateArgumentsTypeError, + null, + typeof(ArgumentException)); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) + { + var target = CreateParser(kind); + + var args = target.Parse(new[] { "-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" }, + CommandLineArgumentErrorCategory.InvalidDictionaryValue, + "NoDuplicateKeys", + typeof(ArgumentException), + remainingArgumentCount: 2); + } - [TestMethod] - public void ParseTest() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestMultiValueSeparator(ProviderKind kind) + { + var target = CreateParser(kind); + + var args = target.Parse(new[] { "-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); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestNameValueSeparator(ProviderKind kind) + { + var target = CreateParser(kind); + CollectionAssert.AreEquivalent(new[] { ':', '=' }, target.NameValueSeparators); + var args = CheckSuccess(target, new[] { "-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" }); + Assert.AreEqual("test", args.Argument1); + Assert.AreEqual("foo:bar", args.Argument2); + args = CheckSuccess(target, new[] { "-Argument2:foo=bar" }); + Assert.AreEqual("foo=bar", args.Argument2); + + CheckThrows(target, + new[] { "-Argument1>test" }, + CommandLineArgumentErrorCategory.UnknownArgument, + "Argument1>test", + remainingArgumentCount: 1); + + var options = new ParseOptions() { - var target = new CommandLineParser(); - - // Only required arguments - TestParse(target, "val1 2 -arg6 val6", "val1", 2, arg6: "val6"); - // Make sure negative numbers are accepted, and not considered an argument name. - TestParse(target, "val1 -2 -arg6 val6", "val1", -2, arg6: "val6"); - // 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 }); - // 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 }); - // 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)); - // Using aliases - TestParse(target, "val1 2 -alias1 valalias6 -alias3", "val1", 2, arg6: "valalias6", arg7: true); - // Long prefix cannot be used - CheckThrows(() => target.Parse(new[] { "val1", "2", "--arg6", "val6" }), target, CommandLineArgumentErrorCategory.UnknownArgument, "-arg6"); - // Short name cannot be used - CheckThrows(() => target.Parse(new[] { "val1", "2", "-arg6", "val6", "-a:5.5" }), target, CommandLineArgumentErrorCategory.UnknownArgument, "a"); - } + NameValueSeparators = new[] { '>' }, + }; + + target = CreateParser(kind, options); + args = target.Parse(new[] { "-Argument1>test", "-Argument2>foo>bar" }); + Assert.IsNotNull(args); + Assert.AreEqual("test", args.Argument1); + Assert.AreEqual("foo>bar", args.Argument2); + CheckThrows(target, + new[] { "-Argument1:test" }, + CommandLineArgumentErrorCategory.UnknownArgument, + "Argument1:test", + remainingArgumentCount: 1); + + CheckThrows(target, + new[] { "-Argument1=test" }, + CommandLineArgumentErrorCategory.UnknownArgument, + "Argument1=test", + remainingArgumentCount: 1); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestKeyValueSeparator(ProviderKind kind) + { + var target = CreateParser(kind); + Assert.AreEqual("=", target.GetArgument("DefaultSeparator")!.DictionaryInfo!.KeyValueSeparator); + Assert.AreEqual("String=Int32", target.GetArgument("DefaultSeparator")!.ValueDescription); + 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<=>" }); + 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" }, + CommandLineArgumentErrorCategory.ArgumentValueConversion, + "CustomSeparator", + typeof(FormatException), + remainingArgumentCount: 2); + + // Inner exception is FormatException because what throws here is trying to convert + // ">bar" to int. + CheckThrows(target, + new[] { "-DefaultSeparator", "foo<=>bar" }, + CommandLineArgumentErrorCategory.ArgumentValueConversion, + "DefaultSeparator", + typeof(FormatException), + remainingArgumentCount: 2); + } - [TestMethod] - public void ParseTestEmptyArguments() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsage(ProviderKind kind) + { + var options = new ParseOptions() { - Type argumentsType = typeof(EmptyArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + ArgumentNamePrefixes = new[] { "/", "-" } + }; - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind, options); + var writer = new UsageWriter() + { + ExecutableName = _executableName + }; - // 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.Parse(new[] { "Foo", "Bar" }), target, CommandLineArgumentErrorCategory.TooManyArguments); - } + string actual = target.GetUsage(writer); + Assert.AreEqual(_expectedDefaultUsage, actual); + } - [TestMethod] - public void ParseTestTooManyArguments() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageLongShort(ProviderKind kind) + { + var target = CreateParser(kind); + var options = new UsageWriter() { - Type argumentsType = typeof(MultipleConstructorsArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + ExecutableName = _executableName + }; - var target = new CommandLineParser(argumentsType, options); + string actual = target.GetUsage(options); + Assert.AreEqual(_expectedLongShortUsage, actual); - // Only accepts one positional argument. - CheckThrows(() => target.Parse(new[] { "Foo", "Bar" }), target, CommandLineArgumentErrorCategory.TooManyArguments); - } + options.UseShortNamesForSyntax = true; + actual = target.GetUsage(options); + Assert.AreEqual(_expectedLongShortUsageShortNameSyntax, actual); - [TestMethod] - public void ParseTestPropertySetterThrows() + options = new UsageWriter() { - Type argumentsType = typeof(MultipleConstructorsArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + ExecutableName = _executableName, + UseAbbreviatedSyntax = true, + }; - var target = new CommandLineParser(argumentsType, options); - - CheckThrows(() => target.Parse(new[] { "Foo", "-ThrowingArgument", "-5" }), - target, - CommandLineArgumentErrorCategory.ApplyValueError, - "ThrowingArgument", - typeof(ArgumentOutOfRangeException)); - } + actual = target.GetUsage(options); + Assert.AreEqual(_expectedLongShortUsageAbbreviated, actual); + } - [TestMethod] - public void ParseTestConstructorThrows() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageFilter(ProviderKind kind) + { + var target = CreateParser(kind); + var options = new UsageWriter() { - Type argumentsType = typeof(MultipleConstructorsArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + ExecutableName = _executableName, + ArgumentDescriptionListFilter = DescriptionListFilterMode.Description + }; - var target = new CommandLineParser(argumentsType, options); + string actual = target.GetUsage(options); + Assert.AreEqual(_expectedUsageDescriptionOnly, actual); - CheckThrows(() => target.Parse(new[] { "invalid" }), - target, - CommandLineArgumentErrorCategory.CreateArgumentsTypeError, - null, - typeof(ArgumentException)); - } + options.ArgumentDescriptionListFilter = DescriptionListFilterMode.All; + actual = target.GetUsage(options); + Assert.AreEqual(_expectedUsageAll, actual); + + options.ArgumentDescriptionListFilter = DescriptionListFilterMode.None; + actual = target.GetUsage(options); + Assert.AreEqual(_expectedUsageNone, actual); + } - [TestMethod] - public void ParseTestDuplicateDictionaryKeys() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageColor(ProviderKind kind) + { + var options = new ParseOptions() { - Type argumentsType = typeof(DictionaryArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); - - DictionaryArguments args = (DictionaryArguments)target.Parse(new[] { "-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.Parse(new[] { "-NoDuplicateKeys", "Foo=1", "-NoDuplicateKeys", "Bar=2", "-NoDuplicateKeys", "Foo=3" }), - target, - CommandLineArgumentErrorCategory.InvalidDictionaryValue, - "NoDuplicateKeys", - typeof(ArgumentException)); - } + ArgumentNamePrefixes = new[] { "/", "-" } + }; - [TestMethod] - public void ParseTestMultiValueSeparator() + CommandLineParser target = CreateParser(kind, options); + var writer = new UsageWriter(useColor: true) { - Type argumentsType = typeof(MultiValueSeparatorArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + ExecutableName = _executableName, + }; - var target = new CommandLineParser(argumentsType, options); + string actual = target.GetUsage(writer); + Assert.AreEqual(_expectedUsageColor, actual); - MultiValueSeparatorArguments args = (MultiValueSeparatorArguments)target.Parse(new[] { "-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); - } + target = CreateParser(kind); + actual = target.GetUsage(writer); + Assert.AreEqual(_expectedLongShortUsageColor, actual); + } - [TestMethod] - public void ParseTestNameValueSeparator() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageOrder(ProviderKind kind) + { + var parser = CreateParser(kind); + var options = new UsageWriter() { - Type argumentsType = typeof(SimpleArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); - Assert.AreEqual(CommandLineParser.DefaultNameValueSeparator, target.NameValueSeparator); - SimpleArguments args = (SimpleArguments)target.Parse(new[] { "-Argument1:test", "-Argument2:foo:bar" }); - Assert.IsNotNull(args); - Assert.AreEqual("test", args.Argument1); - Assert.AreEqual("foo:bar", args.Argument2); - CheckThrows(() => target.Parse(new[] { "-Argument1=test" }), - target, - CommandLineArgumentErrorCategory.UnknownArgument, - "Argument1=test"); - - target.Options.NameValueSeparator = '='; - args = (SimpleArguments)target.Parse(new[] { "-Argument1=test", "-Argument2=foo=bar" }); - Assert.IsNotNull(args); - Assert.AreEqual("test", args.Argument1); - Assert.AreEqual("foo=bar", args.Argument2); - CheckThrows(() => target.Parse(new[] { "-Argument1:test" }), - target, - CommandLineArgumentErrorCategory.UnknownArgument, - "Argument1:test"); - } + ExecutableName = _executableName, + ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical, + }; + + var usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabeticalLongName, usage); + + options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalDescending; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabeticalLongNameDescending, usage); + + options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortName; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabeticalShortName, usage); + + options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortNameDescending; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabeticalShortNameDescending, usage); + + parser = CreateParser(kind, new ParseOptions() { Mode = ParsingMode.Default }); + options.ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabetical, usage); + + options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalDescending; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabeticalDescending, usage); + + // ShortName versions work like regular if not in LongShortMode. + options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortName; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabetical, usage); + + options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortNameDescending; + usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageAlphabeticalDescending, usage); + } - [TestMethod] - public void ParseTestKeyValueSeparator() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageSeparator(ProviderKind kind) + { + var options = new ParseOptions() { - var target = new CommandLineParser(typeof(KeyValueSeparatorArguments)); - Assert.AreEqual("=", target.GetArgument("DefaultSeparator")!.KeyValueSeparator); - Assert.AreEqual("String=Int32", target.GetArgument("DefaultSeparator")!.ValueDescription); - Assert.AreEqual("<=>", target.GetArgument("CustomSeparator")!.KeyValueSeparator); - Assert.AreEqual("String<=>String", target.GetArgument("CustomSeparator")!.ValueDescription); - - var result = (KeyValueSeparatorArguments)target.Parse(new[] { "-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.Parse(new[] { "-CustomSeparator", "foo=bar" }), - target, - CommandLineArgumentErrorCategory.ArgumentValueConversion, - "CustomSeparator", - typeof(FormatException)); - - // Inner exception is Argument exception because what throws here is trying to convert - // ">bar" to int. - CheckThrows(() => target.Parse(new[] { "-DefaultSeparator", "foo<=>bar" }), - target, - CommandLineArgumentErrorCategory.ArgumentValueConversion, - "DefaultSeparator", - ArgumentConversionInner); - } + ArgumentNamePrefixes = new[] { "/", "-" }, + UsageWriter = new UsageWriter() + { + ExecutableName = _executableName, + UseWhiteSpaceValueSeparator = false, + } + }; + var target = CreateParser(kind, options); + string actual = target.GetUsage(options.UsageWriter); + Assert.AreEqual(_expectedUsageSeparator, actual); + } - [TestMethod] - public void TestWriteUsage() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageCustomIndent(ProviderKind kind) + { + var options = new ParseOptions() { - Type argumentsType = typeof(TestArguments); - var options = new ParseOptions() + UsageWriter = new UsageWriter() { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + ExecutableName = _executableName, + ArgumentDescriptionIndent = 4, + } + }; + var target = CreateParser(kind, options); + string actual = target.GetUsage(options.UsageWriter); + Assert.AreEqual(_expectedCustomIndentUsage, actual); + } - var target = new CommandLineParser(argumentsType, options); - var writer = new UsageWriter() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestStaticParse(ProviderKind kind) + { + using var output = new StringWriter(); + using var lineWriter = new LineWrappingTextWriter(output, 0); + using var error = new StringWriter(); + var options = new ParseOptions() + { + ArgumentNamePrefixes = new[] { "/", "-" }, + Error = error, + ShowUsageOnError = UsageHelpRequest.Full, + UsageWriter = new UsageWriter(lineWriter) { - ExecutableName = _executableName - }; + ExecutableName = _executableName, + } + }; + + var result = StaticParse(kind, new[] { "foo", "-Arg6", "bar" }, options); + Assert.IsNotNull(result); + Assert.AreEqual("foo", result.Arg1); + Assert.AreEqual("bar", result.Arg6); + Assert.AreEqual(0, output.ToString().Length); + Assert.AreEqual(0, error.ToString().Length); + + result = StaticParse(kind, Array.Empty(), options); + Assert.IsNull(result); + Assert.IsTrue(error.ToString().Length > 0); + Assert.AreEqual(_expectedDefaultUsage, output.ToString()); + + output.GetStringBuilder().Clear(); + error.GetStringBuilder().Clear(); + result = StaticParse(kind, new[] { "-Help" }, options); + Assert.IsNull(result); + Assert.AreEqual(0, error.ToString().Length); + Assert.AreEqual(_expectedDefaultUsage, output.ToString()); + + options.ShowUsageOnError = UsageHelpRequest.SyntaxOnly; + output.GetStringBuilder().Clear(); + error.GetStringBuilder().Clear(); + result = StaticParse(kind, Array.Empty(), options); + Assert.IsNull(result); + Assert.IsTrue(error.ToString().Length > 0); + Assert.AreEqual(_expectedUsageSyntaxOnly, output.ToString()); + + options.ShowUsageOnError = UsageHelpRequest.None; + output.GetStringBuilder().Clear(); + error.GetStringBuilder().Clear(); + result = StaticParse(kind, Array.Empty(), options); + Assert.IsNull(result); + Assert.IsTrue(error.ToString().Length > 0); + Assert.AreEqual(_expectedUsageMessageOnly, output.ToString()); + + // Still get full help with -Help arg. + output.GetStringBuilder().Clear(); + error.GetStringBuilder().Clear(); + result = StaticParse(kind, new[] { "-Help" }, options); + Assert.IsNull(result); + Assert.AreEqual(0, error.ToString().Length); + Assert.AreEqual(_expectedDefaultUsage, output.ToString()); + } - string actual = target.GetUsage(writer); - Assert.AreEqual(_expectedDefaultUsage, actual); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + 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" }); + Assert.IsNotNull(result); + Assert.IsFalse(parser.HelpRequested); + Assert.IsTrue(result.DoesNotCancel); + Assert.IsFalse(result.DoesCancel); + Assert.AreEqual("foo", result.Argument1); + Assert.AreEqual("bar", result.Argument2); + Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); + Assert.IsNull(parser.ParseResult.ArgumentName); + Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); + + // Cancel if -DoesCancel specified. + result = parser.Parse(new[] { "-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); + Assert.AreEqual("DoesCancel", parser.ParseResult.ArgumentName); + Assert.IsTrue(parser.GetArgument("Argument1")!.HasValue); + Assert.AreEqual("foo", (string?)parser.GetArgument("Argument1")!.Value); + Assert.IsTrue(parser.GetArgument("DoesCancel")!.HasValue); + Assert.IsTrue((bool)parser.GetArgument("DoesCancel")!.Value!); + Assert.IsFalse(parser.GetArgument("DoesNotCancel")!.HasValue); + Assert.IsNull(parser.GetArgument("DoesNotCancel")!.Value); + Assert.IsFalse(parser.GetArgument("Argument2")!.HasValue); + Assert.IsNull(parser.GetArgument("Argument2")!.Value); + + // Use the event handler to cancel on -DoesNotCancel. + static void handler1(object? sender, ArgumentParsedEventArgs e) + { + if (e.Argument.ArgumentName == "DoesNotCancel") + { + e.CancelParsing = CancelMode.Abort; + } } - [TestMethod] - public void TestWriteUsageLongShort() + parser.ArgumentParsed += handler1; + result = parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); + Assert.IsNull(result); + Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); + Assert.IsNull(parser.ParseResult.LastException); + Assert.AreEqual("DoesNotCancel", parser.ParseResult.ArgumentName); + AssertSpanEqual(new[] { "-Argument2", "bar" }.AsSpan(), parser.ParseResult.RemainingArguments.Span); + Assert.IsFalse(parser.HelpRequested); + Assert.IsTrue(parser.GetArgument("Argument1")!.HasValue); + Assert.AreEqual("foo", (string?)parser.GetArgument("Argument1")!.Value); + Assert.IsTrue(parser.GetArgument("DoesNotCancel")!.HasValue); + Assert.IsTrue((bool)parser.GetArgument("DoesNotCancel")!.Value!); + Assert.IsFalse(parser.GetArgument("DoesCancel")!.HasValue); + Assert.IsNull(parser.GetArgument("DoesCancel")!.Value); + Assert.IsFalse(parser.GetArgument("Argument2")!.HasValue); + Assert.IsNull(parser.GetArgument("Argument2")!.Value); + parser.ArgumentParsed -= handler1; + + // Use the event handler to abort cancelling on -DoesCancel. + static void handler2(object? sender, ArgumentParsedEventArgs e) { - var target = new CommandLineParser(); - var options = new UsageWriter() + if (e.Argument.ArgumentName == "DoesCancel") { - ExecutableName = _executableName - }; + Assert.AreEqual(CancelMode.Abort, e.CancelParsing); + e.CancelParsing = CancelMode.None; + } + } - string actual = target.GetUsage(options); - Assert.AreEqual(_expectedLongShortUsage, actual); + parser.ArgumentParsed += handler2; + result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); + Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); + Assert.IsNull(parser.ParseResult.ArgumentName); + Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); + Assert.IsNotNull(result); + Assert.IsFalse(parser.HelpRequested); + Assert.IsFalse(result.DoesNotCancel); + Assert.IsTrue(result.DoesCancel); + Assert.AreEqual("foo", result.Argument1); + Assert.AreEqual("bar", result.Argument2); + + // Automatic help argument should cancel. + result = parser.Parse(new[] { "-Help" }); + Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); + Assert.IsNull(parser.ParseResult.LastException); + Assert.AreEqual("Help", parser.ParseResult.ArgumentName); + Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); + Assert.IsNull(result); + Assert.IsTrue(parser.HelpRequested); + } - options.UseShortNamesForSyntax = true; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedLongShortUsageShortNameSyntax, actual); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCancelParsingSuccess(ProviderKind kind) + { + var parser = CreateParser(kind); + var result = parser.Parse(new[] { "-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); + Assert.IsNotNull(result); + Assert.IsFalse(parser.HelpRequested); + Assert.IsFalse(result.DoesNotCancel); + Assert.IsFalse(result.DoesCancel); + Assert.IsTrue(result.DoesCancelWithSuccess); + Assert.AreEqual("foo", result.Argument1); + Assert.IsNull(result.Argument2); + + // No remaining arguments. + result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancelWithSuccess" }); + Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); + Assert.AreEqual("DoesCancelWithSuccess", parser.ParseResult.ArgumentName); + Assert.AreEqual(0, parser.ParseResult.RemainingArguments.Length); + Assert.IsNotNull(result); + Assert.IsFalse(parser.HelpRequested); + Assert.IsFalse(result.DoesNotCancel); + Assert.IsFalse(result.DoesCancel); + Assert.IsTrue(result.DoesCancelWithSuccess); + Assert.AreEqual("foo", result.Argument1); + Assert.IsNull(result.Argument2); + } - options = new UsageWriter() - { - ExecutableName = _executableName, - UseAbbreviatedSyntax = true, - }; + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestParseOptionsAttribute(ProviderKind kind) + { + var parser = CreateParser(kind); + Assert.IsFalse(parser.AllowWhiteSpaceValueSeparator); + Assert.IsTrue(parser.AllowDuplicateArguments); + CollectionAssert.AreEquivalent(new[] { '=' }, parser.NameValueSeparators); + Assert.AreEqual(ParsingMode.LongShort, parser.Mode); + CollectionAssert.AreEqual(new[] { "--", "-" }, parser.ArgumentNamePrefixes); + Assert.AreEqual("---", parser.LongArgumentNamePrefix); + // Verify case sensitivity. + Assert.IsNull(parser.GetArgument("argument")); + Assert.IsNotNull(parser.GetArgument("Argument")); + // Verify no auto help argument. + Assert.IsNull(parser.GetArgument("Help")); + + // ParseOptions take precedence + var options = new ParseOptions() + { + Mode = ParsingMode.Default, + ArgumentNameComparison = StringComparison.OrdinalIgnoreCase, + AllowWhiteSpaceValueSeparator = true, + DuplicateArguments = ErrorMode.Error, + NameValueSeparators = new[] { ';' }, + ArgumentNamePrefixes = new[] { "+" }, + AutoHelpArgument = true, + }; + + parser = CreateParser(kind, options); + Assert.IsTrue(parser.AllowWhiteSpaceValueSeparator); + Assert.IsFalse(parser.AllowDuplicateArguments); + CollectionAssert.AreEquivalent(new[] { ';' }, parser.NameValueSeparators); + Assert.AreEqual(ParsingMode.Default, parser.Mode); + CollectionAssert.AreEqual(new[] { "+" }, parser.ArgumentNamePrefixes); + Assert.IsNull(parser.LongArgumentNamePrefix); + // Verify case insensitivity. + Assert.IsNotNull(parser.GetArgument("argument")); + Assert.IsNotNull(parser.GetArgument("Argument")); + // Verify auto help argument. + Assert.IsNotNull(parser.GetArgument("Help")); + } - actual = target.GetUsage(options); - Assert.AreEqual(_expectedLongShortUsageAbbreviated, actual); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCulture(ProviderKind kind) + { + var result = StaticParse(kind, new[] { "-Argument", "5.5" }); + Assert.IsNotNull(result); + Assert.AreEqual(5.5, result.Argument); + result = StaticParse(kind, new[] { "-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); + Assert.IsNotNull(result); + Assert.AreEqual(5.5, result.Argument); + result = StaticParse(kind, new[] { "-Argument", "5,5" }); + Assert.IsNotNull(result); + // . was interpreted as a thousands separator. + Assert.AreEqual(55, result.Argument); + } - [TestMethod] - public void TestWriteUsageFilter() - { - var target = new CommandLineParser(); - var options = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionListFilter = DescriptionListFilterMode.Description - }; + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestLongShortMode(ProviderKind kind) + { + var parser = CreateParser(kind); + Assert.AreEqual(ParsingMode.LongShort, parser.Mode); + Assert.AreEqual(CommandLineParser.DefaultLongArgumentNamePrefix, parser.LongArgumentNamePrefix); + CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), parser.ArgumentNamePrefixes); + Assert.AreSame(parser.GetArgument("foo"), parser.GetShortArgument('f')); + Assert.AreSame(parser.GetArgument("arg2"), parser.GetShortArgument('a')); + Assert.AreSame(parser.GetArgument("switch1"), parser.GetShortArgument('s')); + Assert.AreSame(parser.GetArgument("switch2"), parser.GetShortArgument('k')); + Assert.IsNull(parser.GetArgument("switch3")); + Assert.AreEqual("u", parser.GetShortArgument('u')!.ArgumentName); + Assert.AreEqual('f', parser.GetArgument("foo")!.ShortName); + Assert.IsTrue(parser.GetArgument("foo")!.HasShortName); + 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" }); + Assert.AreEqual(5, result.Foo); + Assert.AreEqual(6, result.Bar); + Assert.AreEqual(7, result.Arg2); + Assert.AreEqual(8, result.Arg1); + Assert.IsTrue(result.Switch1); + Assert.IsFalse(result.Switch2); + Assert.IsFalse(result.Switch3); + + // Combine switches. + result = CheckSuccess(parser, new[] { "-su" }); + Assert.IsTrue(result.Switch1); + Assert.IsFalse(result.Switch2); + Assert.IsTrue(result.Switch3); + + // Use a short alias. + result = CheckSuccess(parser, new[] { "-b", "5" }); + Assert.AreEqual(5, result.Arg2); + + // Combining non-switches is an error. + CheckThrows(parser, new[] { "-sf" }, CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, "sf", remainingArgumentCount: 1); + + // Can't use long argument prefix with short names. + CheckThrows(parser, new[] { "--s" }, CommandLineArgumentErrorCategory.UnknownArgument, "s", remainingArgumentCount: 1); + + // And vice versa. + CheckThrows(parser, new[] { "-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); + } - string actual = target.GetUsage(options); - Assert.AreEqual(_expectedUsageDescriptionOnly, actual); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestMethodArguments(ProviderKind kind) + { + var parser = CreateParser(kind); - options.ArgumentDescriptionListFilter = DescriptionListFilterMode.All; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedUsageAll, actual); + Assert.AreEqual(ArgumentKind.Method, parser.GetArgument("NoCancel")!.Kind); + Assert.IsNull(parser.GetArgument("NotAnArgument")); + Assert.IsNull(parser.GetArgument("NotStatic")); + Assert.IsNull(parser.GetArgument("NotPublic")); - options.ArgumentDescriptionListFilter = DescriptionListFilterMode.None; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedUsageNone, actual); - } + CheckSuccess(parser, new[] { "-NoCancel" }); + Assert.AreEqual(nameof(MethodArguments.NoCancel), MethodArguments.CalledMethodName); - [TestMethod] - public void TestWriteUsageColor() - { - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + CheckCanceled(parser, new[] { "-Cancel", "Foo" }, "Cancel", false, 1); + Assert.AreEqual(nameof(MethodArguments.Cancel), MethodArguments.CalledMethodName); - var target = new CommandLineParser(typeof(TestArguments), options); - var writer = new UsageWriter(useColor: true) - { - ExecutableName = _executableName, - }; + CheckCanceled(parser, new[] { "-CancelWithHelp" }, "CancelWithHelp", true, 0); + Assert.AreEqual(nameof(MethodArguments.CancelWithHelp), MethodArguments.CalledMethodName); - string actual = target.GetUsage(writer); - Assert.AreEqual(_expectedUsageColor, actual); + CheckSuccess(parser, new[] { "-CancelWithValue", "1" }); + Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); + Assert.AreEqual(1, MethodArguments.Value); - target = new CommandLineParser(typeof(LongShortArguments)); - actual = target.GetUsage(writer); - Assert.AreEqual(_expectedLongShortUsageColor, actual); - } + CheckCanceled(parser, new[] { "-CancelWithValue", "-1" }, "CancelWithValue", false); + Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); + Assert.AreEqual(-1, MethodArguments.Value); - [TestMethod] - public void TestWriteUsageOrder() - { - var parser = new CommandLineParser(); - var options = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical, - }; + CheckSuccess(parser, new[] { "-CancelWithValueAndHelp", "1" }); + Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); + Assert.AreEqual(1, MethodArguments.Value); - var usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalLongName, usage); + CheckCanceled(parser, new[] { "-CancelWithValueAndHelp", "-1", "bar" }, "CancelWithValueAndHelp", true, 1); + Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); + Assert.AreEqual(-1, MethodArguments.Value); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalLongNameDescending, usage); + CheckSuccess(parser, new[] { "-NoReturn" }); + Assert.AreEqual(nameof(MethodArguments.NoReturn), MethodArguments.CalledMethodName); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortName; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalShortName, usage); + CheckSuccess(parser, new[] { "42" }); + Assert.AreEqual(nameof(MethodArguments.Positional), MethodArguments.CalledMethodName); + Assert.AreEqual(42, MethodArguments.Value); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortNameDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalShortNameDescending, usage); + CheckCanceled(parser, new[] { "-CancelModeAbort", "Foo" }, "CancelModeAbort", false, 1); + Assert.AreEqual(nameof(MethodArguments.CancelModeAbort), MethodArguments.CalledMethodName); - parser = new CommandLineParser(new ParseOptions() { Mode = ParsingMode.Default }); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabetical, usage); + CheckSuccess(parser, new[] { "-CancelModeSuccess", "Foo" }, "CancelModeSuccess", 1); + Assert.AreEqual(nameof(MethodArguments.CancelModeSuccess), MethodArguments.CalledMethodName); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalDescending, usage); + CheckSuccess(parser, new[] { "-CancelModeNone" }); + Assert.AreEqual(nameof(MethodArguments.CancelModeNone), MethodArguments.CalledMethodName); + } - // ShortName versions work like regular if not in LongShortMode. - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortName; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabetical, usage); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAutomaticArgumentConflict(ProviderKind kind) + { + CommandLineParser parser = CreateParser(kind); + VerifyArgument(parser.GetArgument("Help"), new ExpectedArgument("Help", typeof(int))); + VerifyArgument(parser.GetArgument("Version"), new ExpectedArgument("Version", typeof(int))); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortNameDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalDescending, usage); - } + parser = CreateParser(kind); + VerifyArgument(parser.GetShortArgument('?'), new ExpectedArgument("Foo", typeof(int)) { ShortName = '?' }); + } - [TestMethod] - public void TestWriteUsageSeparator() - { - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" }, - UsageWriter = new UsageWriter() - { - ExecutableName = _executableName, - UseWhiteSpaceValueSeparator = false, - } - }; - var target = new CommandLineParser(options); - string actual = target.GetUsage(options.UsageWriter); - Assert.AreEqual(_expectedUsageSeparator, actual); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestHiddenArgument(ProviderKind kind) + { + var parser = CreateParser(kind); - [TestMethod] - public void TestWriteUsageCustomIndent() - { - var options = new ParseOptions() - { - UsageWriter = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionIndent = 4, - } - }; - var target = new CommandLineParser(options); - string actual = target.GetUsage(options.UsageWriter); - Assert.AreEqual(_expectedCustomIndentUsage, actual); - } + // Verify the hidden argument exists. + VerifyArgument(parser.GetArgument("Hidden"), new ExpectedArgument("Hidden", typeof(int)) { IsHidden = true }); - [TestMethod] - public void TestStaticParse() + // Verify it's not in the usage. + var options = new UsageWriter() { - using var output = new StringWriter(); - using var lineWriter = new LineWrappingTextWriter(output, 0); - using var error = new StringWriter(); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" }, - Error = error, - UsageWriter = new UsageWriter(lineWriter) - { - ExecutableName = _executableName, - } - }; - - var result = CommandLineParser.Parse(new[] { "foo", "-Arg6", "bar" }, options); - Assert.IsNotNull(result); - Assert.AreEqual("foo", result.Arg1); - Assert.AreEqual("bar", result.Arg6); - Assert.AreEqual(0, output.ToString().Length); - Assert.AreEqual(0, error.ToString().Length); - - result = CommandLineParser.Parse(Array.Empty(), options); - Assert.IsNull(result); - Assert.IsTrue(error.ToString().Length > 0); - Assert.AreEqual(_expectedDefaultUsage, output.ToString()); - - output.GetStringBuilder().Clear(); - error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(new[] { "-Help" }, options); - Assert.IsNull(result); - Assert.AreEqual(0, error.ToString().Length); - Assert.AreEqual(_expectedDefaultUsage, output.ToString()); - - options.ShowUsageOnError = UsageHelpRequest.SyntaxOnly; - output.GetStringBuilder().Clear(); - error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(Array.Empty(), options); - Assert.IsNull(result); - Assert.IsTrue(error.ToString().Length > 0); - Assert.AreEqual(_expectedUsageSyntaxOnly, output.ToString()); - - options.ShowUsageOnError = UsageHelpRequest.None; - output.GetStringBuilder().Clear(); - error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(Array.Empty(), options); - Assert.IsNull(result); - Assert.IsTrue(error.ToString().Length > 0); - Assert.AreEqual(_expectedUsageMessageOnly, output.ToString()); - - // Still get full help with -Help arg. - output.GetStringBuilder().Clear(); - error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(new[] { "-Help" }, options); - Assert.IsNull(result); - Assert.AreEqual(0, error.ToString().Length); - Assert.AreEqual(_expectedDefaultUsage, output.ToString()); - } + ExecutableName = _executableName, + ArgumentDescriptionListFilter = DescriptionListFilterMode.All, + }; - [TestMethod] - public void TestCancelParsing() - { - var parser = new CommandLineParser(typeof(CancelArguments)); - - // Don't cancel if -DoesCancel not specified. - var result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); - Assert.IsNotNull(result); - Assert.IsFalse(parser.HelpRequested); - Assert.IsTrue(result.DoesNotCancel); - Assert.IsFalse(result.DoesCancel); - Assert.AreEqual("foo", result.Argument1); - Assert.AreEqual("bar", result.Argument2); - - // Cancel if -DoesCancel specified. - result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); - Assert.IsNull(result); - Assert.IsTrue(parser.HelpRequested); - Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); - Assert.IsNull(parser.ParseResult.LastException); - Assert.AreEqual("DoesCancel", parser.ParseResult.ArgumentName); - Assert.IsTrue(parser.GetArgument("Argument1").HasValue); - Assert.AreEqual("foo", (string)parser.GetArgument("Argument1").Value); - Assert.IsTrue(parser.GetArgument("DoesCancel").HasValue); - Assert.IsTrue((bool)parser.GetArgument("DoesCancel").Value); - Assert.IsFalse(parser.GetArgument("DoesNotCancel").HasValue); - Assert.IsNull(parser.GetArgument("DoesNotCancel").Value); - Assert.IsFalse(parser.GetArgument("Argument2").HasValue); - Assert.IsNull(parser.GetArgument("Argument2").Value); - - // Use the event handler to cancel on -DoesNotCancel. - static void handler1(object sender, ArgumentParsedEventArgs e) - { - if (e.Argument.ArgumentName == "DoesNotCancel") - { - e.Cancel = true; - } - } + var usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageHidden, usage); + } - parser.ArgumentParsed += handler1; - result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); - Assert.IsNull(result); - Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); - Assert.IsNull(parser.ParseResult.LastException); - Assert.AreEqual("DoesNotCancel", parser.ParseResult.ArgumentName); - Assert.IsTrue(parser.HelpRequested); - Assert.IsTrue(parser.GetArgument("Argument1").HasValue); - Assert.AreEqual("foo", (string)parser.GetArgument("Argument1").Value); - Assert.IsTrue(parser.GetArgument("DoesNotCancel").HasValue); - Assert.IsTrue((bool)parser.GetArgument("DoesNotCancel").Value); - Assert.IsFalse(parser.GetArgument("DoesCancel").HasValue); - Assert.IsNull(parser.GetArgument("DoesCancel").Value); - Assert.IsFalse(parser.GetArgument("Argument2").HasValue); - Assert.IsNull(parser.GetArgument("Argument2").Value); - parser.ArgumentParsed -= handler1; - - // Use the event handler to abort cancelling on -DoesCancel. - static void handler2(object sender, ArgumentParsedEventArgs e) - { - if (e.Argument.ArgumentName == "DoesCancel") - { - e.OverrideCancelParsing = true; - } - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformPascalCase(ProviderKind kind) + { + var options = new ParseOptions + { + ArgumentNameTransform = NameTransform.PascalCase + }; - parser.ArgumentParsed += handler2; - result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); - Assert.IsNotNull(result); - Assert.IsFalse(parser.HelpRequested); - Assert.IsFalse(result.DoesNotCancel); - Assert.IsTrue(result.DoesCancel); - Assert.AreEqual("foo", result.Argument1); - Assert.AreEqual("bar", result.Argument2); - - // Automatic help argument should cancel. - result = (CancelArguments)parser.Parse(new[] { "-Help" }); - Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); - Assert.IsNull(parser.ParseResult.LastException); - Assert.AreEqual("Help", parser.ParseResult.ArgumentName); - Assert.IsNull(result); - Assert.IsTrue(parser.HelpRequested); - } + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] + { + 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("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] - public void TestParseOptionsAttribute() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformCamelCase(ProviderKind kind) + { + var options = new ParseOptions { - var parser = new CommandLineParser(typeof(ParseOptionsArguments)); - Assert.IsFalse(parser.AllowWhiteSpaceValueSeparator); - Assert.IsTrue(parser.AllowDuplicateArguments); - Assert.AreEqual('=', parser.NameValueSeparator); - Assert.AreEqual(ParsingMode.LongShort, parser.Mode); - CollectionAssert.AreEqual(new[] { "--", "-" }, parser.ArgumentNamePrefixes); - Assert.AreEqual("---", parser.LongArgumentNamePrefix); - // Verify case sensitivity. - Assert.IsNull(parser.GetArgument("argument")); - Assert.IsNotNull(parser.GetArgument("Argument")); - // Verify no auto help argument. - Assert.IsNull(parser.GetArgument("Help")); - - // ParseOptions take precedence - var options = new ParseOptions() - { - Mode = ParsingMode.Default, - ArgumentNameComparer = StringComparer.OrdinalIgnoreCase, - AllowWhiteSpaceValueSeparator = true, - DuplicateArguments = ErrorMode.Error, - NameValueSeparator = ';', - ArgumentNamePrefixes = new[] { "+" }, - AutoHelpArgument = true, - }; - - parser = new CommandLineParser(typeof(ParseOptionsArguments), options); - Assert.IsTrue(parser.AllowWhiteSpaceValueSeparator); - Assert.IsFalse(parser.AllowDuplicateArguments); - Assert.AreEqual(';', parser.NameValueSeparator); - Assert.AreEqual(ParsingMode.Default, parser.Mode); - CollectionAssert.AreEqual(new[] { "+" }, parser.ArgumentNamePrefixes); - Assert.IsNull(parser.LongArgumentNamePrefix); - // Verify case insensitivity. - Assert.IsNotNull(parser.GetArgument("argument")); - Assert.IsNotNull(parser.GetArgument("Argument")); - // Verify auto help argument. - Assert.IsNotNull(parser.GetArgument("Help")); - } + ArgumentNameTransform = NameTransform.CamelCase + }; - [TestMethod] - public void TestCulture() + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - var result = CommandLineParser.Parse(new[] { "-Argument", "5.5" }); - Assert.IsNotNull(result); - Assert.AreEqual(5.5, result.Argument); - Assert.IsNull(CommandLineParser.Parse(new[] { "-Argument", "5,5" })); - - var options = new ParseOptions { Culture = new CultureInfo("nl-NL") }; - result = CommandLineParser.Parse(new[] { "-Argument", "5,5" }, options); - Assert.IsNotNull(result); - Assert.AreEqual(5.5, result.Argument); - Assert.IsNull(CommandLineParser.Parse(new[] { "-Argument", "5.5" }, options)); - } + 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("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] - public void TestLongShortMode() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformSnakeCase(ProviderKind kind) + { + var options = new ParseOptions { - var parser = new CommandLineParser(); - Assert.AreEqual(ParsingMode.LongShort, parser.Mode); - Assert.AreEqual(CommandLineParser.DefaultLongArgumentNamePrefix, parser.LongArgumentNamePrefix); - CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), parser.ArgumentNamePrefixes); - Assert.AreSame(parser.GetArgument("foo"), parser.GetShortArgument('f')); - Assert.AreSame(parser.GetArgument("arg2"), parser.GetShortArgument('a')); - Assert.AreSame(parser.GetArgument("switch1"), parser.GetShortArgument('s')); - Assert.AreSame(parser.GetArgument("switch2"), parser.GetShortArgument('k')); - Assert.IsNull(parser.GetArgument("switch3")); - Assert.AreEqual("u", parser.GetShortArgument('u').ArgumentName); - Assert.AreEqual('f', parser.GetArgument("foo").ShortName); - Assert.IsTrue(parser.GetArgument("foo").HasShortName); - Assert.AreEqual('\0', parser.GetArgument("bar").ShortName); - Assert.IsFalse(parser.GetArgument("bar").HasShortName); - - var result = parser.Parse(new[] { "-f", "5", "--bar", "6", "-a", "7", "--arg1", "8", "-s" }); - Assert.AreEqual(5, result.Foo); - Assert.AreEqual(6, result.Bar); - Assert.AreEqual(7, result.Arg2); - Assert.AreEqual(8, result.Arg1); - Assert.IsTrue(result.Switch1); - Assert.IsFalse(result.Switch2); - Assert.IsFalse(result.Switch3); - - // Combine switches. - result = parser.Parse(new[] { "-su" }); - Assert.IsTrue(result.Switch1); - Assert.IsFalse(result.Switch2); - Assert.IsTrue(result.Switch3); - - // Use a short alias. - result = parser.Parse(new[] { "-b", "5" }); - Assert.AreEqual(5, result.Arg2); - - // Combining non-switches is an error. - CheckThrows(() => parser.Parse(new[] { "-sf" }), parser, CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, "sf"); - - // Can't use long argument prefix with short names. - CheckThrows(() => parser.Parse(new[] { "--s" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "s"); - - // And vice versa. - CheckThrows(() => parser.Parse(new[] { "-Switch1" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "w"); - - // Short alias is ignored on an argument without a short name. - CheckThrows(() => parser.Parse(new[] { "-c" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "c"); - } + ArgumentNameTransform = NameTransform.SnakeCase + }; - [TestMethod] - public void TestMethodArguments() + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - var parser = new CommandLineParser(); + 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("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 }, + }); + } - Assert.AreEqual(ArgumentKind.Method, parser.GetArgument("NoCancel").Kind); - Assert.IsNull(parser.GetArgument("NotAnArgument")); - Assert.IsNull(parser.GetArgument("NotStatic")); - Assert.IsNull(parser.GetArgument("NotPublic")); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformDashCase(ProviderKind kind) + { + var options = new ParseOptions + { + ArgumentNameTransform = NameTransform.DashCase + }; - Assert.IsNotNull(parser.Parse(new[] { "-NoCancel" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.NoCancel), MethodArguments.CalledMethodName); + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] + { + 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("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 }, + }); + } - Assert.IsNull(parser.Parse(new[] { "-Cancel" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.Cancel), MethodArguments.CalledMethodName); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValueDescriptionTransform(ProviderKind kind) + { + var options = new ParseOptions + { + ValueDescriptionTransform = NameTransform.DashCase + }; - Assert.IsNull(parser.Parse(new[] { "-CancelWithHelp" })); - Assert.IsTrue(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.CancelWithHelp), MethodArguments.CalledMethodName); + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] + { + 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("Version", typeof(bool), ArgumentKind.Method) { ValueDescription = "boolean", MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, + }); + } - Assert.IsNotNull(parser.Parse(new[] { "-CancelWithValue", "1" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); - Assert.AreEqual(1, MethodArguments.Value); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValidation(ProviderKind kind) + { + // Reset for multiple runs. + ValidationArguments.Arg3Value = 0; + 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" }); + Assert.AreEqual(1, result.Arg1); + result = CheckSuccess(parser, new[] { "-Arg1", "5" }); + Assert.AreEqual(5, result.Arg1); + CheckThrows(parser, new[] { "-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[] { " " }); + Assert.AreEqual(" ", result.Arg2); + + // Multiple validators on method + CheckThrows(parser, new[] { "-Arg3", "1238" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); + Assert.AreEqual(0, ValidationArguments.Arg3Value); + CheckThrows(parser, new[] { "-Arg3", "123" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); + Assert.AreEqual(0, ValidationArguments.Arg3Value); + CheckThrows(parser, new[] { "-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" }); + 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" }); + CollectionAssert.AreEqual(new[] { "foo", "bar" }, result.Arg4); + result = CheckSuccess(parser, new[] { "-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" }); + 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" }); + 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" }); + Assert.AreEqual(DayOfWeek.Monday, result.Day2); + result = CheckSuccess(parser, new[] { "-Day2", "" }); + Assert.IsNull(result.Day2); + + // NotNull validator with Nullable. + CheckThrows(parser, new[] { "-NotNull", "" }, CommandLineArgumentErrorCategory.ValidationFailed, "NotNull", remainingArgumentCount: 2); + } - Assert.IsNull(parser.Parse(new[] { "-CancelWithValue", "-1" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); - Assert.AreEqual(-1, MethodArguments.Value); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + 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" }); + 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" }); + 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" }); + Assert.AreEqual(IPAddress.Loopback, result.Address); + Assert.AreEqual(10, result.Throughput); + Assert.AreEqual(1, result.Protocol); + } - Assert.IsNotNull(parser.Parse(new[] { "-CancelWithValueAndHelp", "1" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); - Assert.AreEqual(1, MethodArguments.Value); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestProhibits(ProviderKind kind) + { + var parser = CreateParser(kind); - Assert.IsNull(parser.Parse(new[] { "-CancelWithValueAndHelp", "-1" })); - Assert.IsTrue(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); - Assert.AreEqual(-1, MethodArguments.Value); + var result = CheckSuccess(parser, new[] { "-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"); + } - Assert.IsNotNull(parser.Parse(new[] { "-NoReturn" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.NoReturn), MethodArguments.CalledMethodName); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestRequiresAny(ProviderKind kind) + { + var parser = CreateParser(kind); - Assert.IsNotNull(parser.Parse(new[] { "42" })); - Assert.IsFalse(parser.HelpRequested); - Assert.AreEqual(nameof(MethodArguments.Positional), MethodArguments.CalledMethodName); - Assert.AreEqual(42, MethodArguments.Value); - } + // No need to check if the arguments work indivially since TestRequires and TestProhibits already did that. + CheckThrows(parser, Array.Empty(), CommandLineArgumentErrorCategory.MissingRequiredArgument); + } - [TestMethod] - public void TestAutomaticArgumentConflict() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValidatorUsageHelp(ProviderKind kind) + { + CommandLineParser parser = CreateParser(kind); + var options = new UsageWriter() { - var parser = new CommandLineParser(typeof(AutomaticConflictingNameArguments)); - TestArgument(parser.GetArgument("Help"), new ExpectedArgument("Help", typeof(int))); - TestArgument(parser.GetArgument("Version"), new ExpectedArgument("Version", typeof(int))); + ExecutableName = _executableName, + }; - parser = new CommandLineParser(typeof(AutomaticConflictingShortNameArguments)); - TestArgument(parser.GetShortArgument('?'), new ExpectedArgument("Foo", typeof(int)) { ShortName = '?' }); - } + Assert.AreEqual(_expectedUsageValidators, parser.GetUsage(options)); - [TestMethod] - public void TestHiddenArgument() - { - var parser = new CommandLineParser(); + parser = CreateParser(kind); + Assert.AreEqual(_expectedUsageDependencies, parser.GetUsage(options)); - // Verify the hidden argument exists. - TestArgument(parser.GetArgument("Hidden"), new ExpectedArgument("Hidden", typeof(int)) { IsHidden = true }); + options.IncludeValidatorsInDescription = false; + Assert.AreEqual(_expectedUsageDependenciesDisabled, parser.GetUsage(options)); + } - // Verify it's not in the usage. - var options = new UsageWriter() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestDefaultValueDescriptions(ProviderKind kind) + { + var options = new ParseOptions() + { + DefaultValueDescriptions = new Dictionary() { - ExecutableName = _executableName, - ArgumentDescriptionListFilter = DescriptionListFilterMode.All, - }; + { typeof(bool), "Switch" }, + { typeof(int), "Number" }, + }, + }; + + var parser = CreateParser(kind, options); + Assert.AreEqual("Switch", parser.GetArgument("Arg7")!.ValueDescription); + Assert.AreEqual("Number", parser.GetArgument("Arg9")!.ValueDescription); + Assert.AreEqual("String=Number", parser.GetArgument("Arg13")!.ValueDescription); + } - var usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageHidden, usage); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) + { + var parser = CreateParser(kind); + Assert.IsTrue(parser.GetArgument("Multi")!.MultiValueInfo!.AllowWhiteSpaceSeparator); + 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" }); + 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" }); + 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); + parser.Options.AllowWhiteSpaceValueSeparator = false; + CheckThrows(parser, new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 5); + } - [TestMethod] - public void TestNameTransformPascalCase() - { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.PascalCase - }; + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestInjection(ProviderKind kind) + { + var parser = CreateParser(kind); + var result = CheckSuccess(parser, new[] { "-Arg", "1" }); + Assert.AreSame(parser, result.Parser); + Assert.AreEqual(1, result.Arg); + } - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestDuplicateArguments(ProviderKind kind) + { + var parser = CreateParser(kind); + CheckThrows(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); + parser.Options.DuplicateArguments = ErrorMode.Allow; + var result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); + Assert.AreEqual("bar", result.Argument1); + + bool handlerCalled = false; + bool keepOldValue = false; + void handler(object? sender, DuplicateArgumentEventArgs e) + { + Assert.AreEqual("Argument1", e.Argument.ArgumentName); + Assert.AreEqual("foo", e.Argument.Value); + Assert.AreEqual("bar", e.NewValue); + handlerCalled = true; + if (keepOldValue) { - 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("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 }, - }); + e.KeepOldValue = true; + } } - [TestMethod] - public void TestNameTransformCamelCase() - { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.CamelCase - }; + parser.DuplicateArgument += handler; + + // 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); + Assert.IsFalse(handlerCalled); + + // Now it is called. + parser.Options.DuplicateArguments = ErrorMode.Allow; + result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); + Assert.AreEqual("bar", result.Argument1); + Assert.IsTrue(handlerCalled); + + // Also called for warning, and keep the old value. + parser.Options.DuplicateArguments = ErrorMode.Warning; + handlerCalled = false; + keepOldValue = true; + result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); + Assert.AreEqual("foo", result.Argument1); + Assert.IsTrue(handlerCalled); + } - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] - { - 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("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] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestConversion(ProviderKind kind) + { + var parser = CreateParser(kind); + var result = CheckSuccess(parser, "-ParseCulture 1 -ParseStruct 2 -Ctor 3 -ParseNullable 4 -ParseMulti 5 6 -ParseNullableMulti 7 8 -NullableMulti 9 10 -Nullable 11".Split(' ')); + Assert.AreEqual(1, result.ParseCulture.Value); + Assert.AreEqual(2, result.ParseStruct.Value); + Assert.AreEqual(3, result.Ctor.Value); + Assert.AreEqual(4, result.ParseNullable!.Value.Value); + Assert.AreEqual(5, result.ParseMulti[0].Value); + Assert.AreEqual(6, result.ParseMulti[1].Value); + Assert.AreEqual(7, result.ParseNullableMulti[0]!.Value.Value); + Assert.AreEqual(8, result.ParseNullableMulti[1]!.Value.Value); + Assert.AreEqual(9, result.NullableMulti[0]!.Value); + Assert.AreEqual(10, result.NullableMulti[1]!.Value); + Assert.AreEqual(11, result.Nullable); + + result = CheckSuccess(parser, new[] { "-ParseNullable", "", "-NullableMulti", "1", "", "2", "-ParseNullableMulti", "3", "", "4" }); + Assert.IsNull(result.ParseNullable); + Assert.AreEqual(1, result.NullableMulti[0]!.Value); + Assert.IsNull(result.NullableMulti[1]); + Assert.AreEqual(2, result.NullableMulti[2]!.Value); + Assert.AreEqual(3, result.ParseNullableMulti[0]!.Value.Value); + Assert.IsNull(result.ParseNullableMulti[1]!); + Assert.AreEqual(4, result.ParseNullableMulti[2]!.Value.Value); +#if NET7_0_OR_GREATER + Assert.IsInstanceOfType(((NullableConverter)parser.GetArgument("Nullable")!.Converter).BaseConverter, typeof(SpanParsableConverter)); +#endif + } - [TestMethod] - public void TestNameTransformSnakeCase() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + 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); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + 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[] { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.SnakeCase - }; + 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("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, + }); + } - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] - { - 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("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] + public void TestInitializerDefaultValues() + { + var parser = InitializerDefaultValueArguments.CreateParser(); + Assert.AreEqual("foo\tbar\"", parser.GetArgument("Arg1")!.DefaultValue); + Assert.AreEqual(5.5f, parser.GetArgument("Arg2")!.DefaultValue); + Assert.AreEqual(int.MaxValue, parser.GetArgument("Arg3")!.DefaultValue); + Assert.AreEqual(DayOfWeek.Tuesday, parser.GetArgument("Arg4")!.DefaultValue); + Assert.AreEqual(47, parser.GetArgument("Arg5")!.DefaultValue); + // Does not use a supported expression type. + Assert.IsNull(parser.GetArgument("Arg6")!.DefaultValue); + Assert.AreEqual(0, parser.GetArgument("Arg7")!.DefaultValue); + // Null because set to "default". + Assert.IsNull(parser.GetArgument("Arg8")!.DefaultValue); + // Null because explicit null. + Assert.IsNull(parser.GetArgument("Arg9")!.DefaultValue); + // Null because IncludeDefaultInUsageHelp is false. + Assert.IsNull(parser.GetArgument("Arg10")!.DefaultValue); + } - [TestMethod] - public void TestNameTransformDashCase() - { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.DashCase - }; + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAutoPrefixAliases(ProviderKind kind) + { + var parser = CreateParser(kind); + + // Shortest possible prefixes + var result = parser.Parse(new[] { "-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); + + // Ambiguous due to alias. + CheckThrows(parser, new[] { "-pr", "foo" }, CommandLineArgumentErrorCategory.UnknownArgument, "pr", remainingArgumentCount: 2); + + // Prefix of an alias. + result = parser.Parse(new[] { "-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); + } - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] - { - 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("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] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestApplicationFriendlyName(ProviderKind kind) + { + CommandLineParser parser = CreateParser(kind); + Assert.AreEqual("Friendly name", parser.ApplicationFriendlyName); - [TestMethod] - public void TestValueDescriptionTransform() - { - var options = new ParseOptions - { - ValueDescriptionTransform = NameTransform.DashCase - }; + // Default to assembly title if no friendly name. + parser = CreateParser(kind); + Assert.AreEqual("Ookii.CommandLine Unit Tests", parser.ApplicationFriendlyName); - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] - { - 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("Version", typeof(bool), ArgumentKind.Method) { ValueDescription = "boolean", MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, - }); - } + parser = CreateParser(kind); + Assert.AreEqual("Ookii.CommandLine.Tests.Commands", parser.ApplicationFriendlyName); + } - [TestMethod] - public void TestValidation() + [TestMethod] + public void TestAutoPosition() + { + var parser = AutoPositionArguments.CreateParser(); + VerifyArguments(parser.Arguments, new[] { - var parser = new CommandLineParser(); - - // Range validator on property - CheckThrows(() => parser.Parse(new[] { "-Arg1", "0" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1"); - var result = parser.Parse(new[] { "-Arg1", "1" }); - Assert.AreEqual(1, result.Arg1); - result = parser.Parse(new[] { "-Arg1", "5" }); - Assert.AreEqual(5, result.Arg1); - CheckThrows(() => parser.Parse(new[] { "-Arg1", "6" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1"); - - // Not null or empty on ctor parameter - CheckThrows(() => parser.Parse(new[] { "" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "arg2"); - result = parser.Parse(new[] { " " }); - Assert.AreEqual(" ", result.Arg2); - - // Multiple validators on method - CheckThrows(() => parser.Parse(new[] { "-Arg3", "1238" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3"); - Assert.AreEqual(0, ValidationArguments.Arg3Value); - CheckThrows(() => parser.Parse(new[] { "-Arg3", "123" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3"); - Assert.AreEqual(0, ValidationArguments.Arg3Value); - CheckThrows(() => parser.Parse(new[] { "-Arg3", "7001" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3"); - // Range validation is done after setting the value, so this was set! - Assert.AreEqual(7001, ValidationArguments.Arg3Value); - parser.Parse(new[] { "-Arg3", "1023" }); - Assert.AreEqual(1023, ValidationArguments.Arg3Value); - - // Validator on multi-value argument - CheckThrows(() => parser.Parse(new[] { "-Arg4", "foo;bar;bazz" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); - CheckThrows(() => parser.Parse(new[] { "-Arg4", "foo", "-Arg4", "bar", "-Arg4", "bazz" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); - result = parser.Parse(new[] { "-Arg4", "foo;bar" }); - CollectionAssert.AreEqual(new[] { "foo", "bar" }, result.Arg4); - result = parser.Parse(new[] { "-Arg4", "foo", "-Arg4", "bar" }); - CollectionAssert.AreEqual(new[] { "foo", "bar" }, result.Arg4); - - // Count validator - CheckThrows(() => parser.Parse(new[] { "-Arg4", "foo" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); - CheckThrows(() => parser.Parse(new[] { "-Arg4", "foo;bar;baz;ban;bap" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg4"); - result = parser.Parse(new[] { "-Arg4", "foo;bar;baz;ban" }); - CollectionAssert.AreEqual(new[] { "foo", "bar", "baz", "ban" }, result.Arg4); - - // Enum validator - CheckThrows(() => parser.Parse(new[] { "-Day", "foo" }), parser, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException)); - CheckThrows(() => parser.Parse(new[] { "-Day", "9" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Day"); - CheckThrows(() => parser.Parse(new[] { "-Day", "" }), parser, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException)); - result = parser.Parse(new[] { "-Day", "1" }); - Assert.AreEqual(DayOfWeek.Monday, result.Day); - CheckThrows(() => parser.Parse(new[] { "-Day2", "foo" }), parser, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(FormatException)); - CheckThrows(() => parser.Parse(new[] { "-Day2", "9" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Day2"); - result = parser.Parse(new[] { "-Day2", "1" }); - Assert.AreEqual(DayOfWeek.Monday, result.Day2); - result = parser.Parse(new[] { "-Day2", "" }); - Assert.IsNull(result.Day2); - - // NotNull validator with Nullable. - CheckThrows(() => parser.Parse(new[] { "-NotNull", "" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "NotNull"); - } - - [TestMethod] - public void TestRequires() + 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("Version", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticVersion", Description = "Displays version information.", IsSwitch = true }, + }); + + try { - var parser = new CommandLineParser(); - - var result = parser.Parse(new[] { "-Address", "127.0.0.1" }); - Assert.AreEqual(IPAddress.Loopback, result.Address); - CheckThrows(() => parser.Parse(new[] { "-Port", "9000" }), parser, CommandLineArgumentErrorCategory.DependencyFailed, "Port"); - result = parser.Parse(new[] { "-Address", "127.0.0.1", "-Port", "9000" }); - Assert.AreEqual(IPAddress.Loopback, result.Address); - Assert.AreEqual(9000, result.Port); - CheckThrows(() => parser.Parse(new[] { "-Protocol", "1" }), parser, CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); - CheckThrows(() => parser.Parse(new[] { "-Address", "127.0.0.1", "-Protocol", "1" }), parser, CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); - CheckThrows(() => parser.Parse(new[] { "-Throughput", "10", "-Protocol", "1" }), parser, CommandLineArgumentErrorCategory.DependencyFailed, "Protocol"); - result = parser.Parse(new[] { "-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); + parser = new CommandLineParser(); + Debug.Fail("Expected exception not thrown."); } - - [TestMethod] - public void TestProhibits() + catch (NotSupportedException) { - var parser = new CommandLineParser(); - - var result = parser.Parse(new[] { "-Path", "test" }); - Assert.AreEqual("test", result.Path.Name); - CheckThrows(() => parser.Parse(new[] { "-Path", "test", "-Address", "127.0.0.1" }), parser, CommandLineArgumentErrorCategory.DependencyFailed, "Path"); } + } - [TestMethod] - public void TestRequiresAny() + private class ExpectedArgument + { + public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) { - var parser = new CommandLineParser(); - - // No need to check if the arguments work indivially since TestRequires and TestProhibits already did that. - CheckThrows(() => parser.Parse(Array.Empty()), parser, CommandLineArgumentErrorCategory.MissingRequiredArgument); + Name = name; + Type = type; + Kind = kind; } - [TestMethod] - public void TestValidatorUsageHelp() - { - CommandLineParser parser = new CommandLineParser(); - var options = new UsageWriter() - { - ExecutableName = _executableName, - }; - - Assert.AreEqual(_expectedUsageValidators, parser.GetUsage(options)); - - parser = new CommandLineParser(); - Assert.AreEqual(_expectedUsageDependencies, parser.GetUsage(options)); + public string Name { get; set; } + public string? MemberName { get; set; } + public Type Type { get; set; } + public Type? ElementType { get; set; } + public int? Position { get; set; } + public bool IsRequired { get; set; } + public object? DefaultValue { get; set; } + public string? Description { get; set; } + public string? ValueDescription { get; set; } + public bool IsSwitch { get; set; } + public ArgumentKind Kind { get; set; } + public string[]? Aliases { get; set; } + public char? ShortName { get; set; } + public char[]? ShortAliases { get; set; } + public bool IsHidden { get; set; } + } - options.IncludeValidatorsInDescription = false; - Assert.AreEqual(_expectedUsageDependenciesDisabled, parser.GetUsage(options)); - } + private static void VerifyArgument(CommandLineArgument? argument, ExpectedArgument expected) + { + Assert.IsNotNull(argument); + Assert.AreEqual(expected.Name, argument.ArgumentName); + Assert.AreEqual(expected.MemberName ?? expected.Name, argument.MemberName); + Assert.AreEqual(expected.ShortName.HasValue, argument.HasShortName); + Assert.AreEqual(expected.ShortName ?? '\0', argument.ShortName); + Assert.AreEqual(expected.Type, argument.ArgumentType); + Assert.AreEqual(expected.ElementType ?? expected.Type, argument.ElementType); + Assert.AreEqual(expected.Position, argument.Position); + Assert.AreEqual(expected.IsRequired, argument.IsRequired); + Assert.AreEqual(expected.Description ?? string.Empty, argument.Description); + Assert.AreEqual(expected.ValueDescription ?? argument.ElementType.Name, argument.ValueDescription); + Assert.AreEqual(expected.Kind, argument.Kind); + Assert.AreEqual(expected.Kind is ArgumentKind.MultiValue or ArgumentKind.Dictionary, argument.MultiValueInfo != null); + Assert.AreEqual(expected.Kind == ArgumentKind.Dictionary, argument.DictionaryInfo != null); + Assert.AreEqual(expected.IsSwitch, argument.IsSwitch); + Assert.AreEqual(expected.DefaultValue, argument.DefaultValue); + Assert.AreEqual(expected.IsHidden, argument.IsHidden); + Assert.IsFalse(argument.MultiValueInfo?.AllowWhiteSpaceSeparator ?? false); + Assert.IsNull(argument.Value); + Assert.IsFalse(argument.HasValue); + CollectionAssert.AreEqual(expected.Aliases ?? Array.Empty(), argument.Aliases); + CollectionAssert.AreEqual(expected.ShortAliases ?? Array.Empty(), argument.ShortAliases); + } - [TestMethod] - public void TestDefaultValueDescriptions() + private static void VerifyArguments(IEnumerable arguments, ExpectedArgument[] expected) + { + int index = 0; + foreach (var arg in arguments) { - var options = new ParseOptions() - { - DefaultValueDescriptions = new Dictionary() - { - { typeof(bool), "Switch" }, - { typeof(int), "Number" }, - }, - }; - - var parser = new CommandLineParser(options); - Assert.AreEqual("Switch", parser.GetArgument("Arg7").ValueDescription); - Assert.AreEqual("Number", parser.GetArgument("Arg9").ValueDescription); - Assert.AreEqual("String=Number", parser.GetArgument("Arg13").ValueDescription); + Assert.IsTrue(index < expected.Length, "Too many arguments."); + VerifyArgument(arg, expected[index]); + ++index; } - [TestMethod] - public void TestMultiValueWhiteSpaceSeparator() + Assert.AreEqual(expected.Length, index); + } + + private static void TestParse(CommandLineParser target, string commandLine, string? arg1 = null, int arg2 = 42, bool notSwitch = false, string? arg3 = null, int arg4 = 47, float arg5 = 1.0f, string? arg6 = null, bool arg7 = false, DayOfWeek[]? arg8 = null, int? arg9 = null, bool[]? arg10 = null, bool? arg11 = null, int[]? arg12 = null, Dictionary? arg13 = null, Dictionary? arg14 = null, KeyValuePair? arg15 = null) + { + string[] args = commandLine.Split(' '); // not using quoted arguments in the tests, so this is fine. + var result = target.Parse(args); + Assert.IsNotNull(result); + Assert.AreEqual(ParseStatus.Success, target.ParseResult.Status); + Assert.IsNull(target.ParseResult.LastException); + Assert.IsNull(target.ParseResult.ArgumentName); + Assert.AreEqual(0, target.ParseResult.RemainingArguments.Length); + Assert.IsFalse(target.HelpRequested); + Assert.AreEqual(arg1, result.Arg1); + Assert.AreEqual(arg2, result.Arg2); + Assert.AreEqual(arg3, result.Arg3); + Assert.AreEqual(arg4, result.Arg4); + Assert.AreEqual(arg5, result.Arg5); + Assert.AreEqual(arg6, result.Arg6); + Assert.AreEqual(arg7, result.Arg7); + CollectionAssert.AreEqual(arg8, result.Arg8); + Assert.AreEqual(arg9, result.Arg9); + CollectionAssert.AreEqual(arg10, result.Arg10); + Assert.AreEqual(arg11, result.Arg11); + Assert.AreEqual(notSwitch, result.NotSwitch); + if (arg12 == null) { - var parser = new CommandLineParser(); - Assert.IsTrue(parser.GetArgument("Multi").AllowMultiValueWhiteSpaceSeparator); - Assert.IsFalse(parser.GetArgument("MultiSwitch").AllowMultiValueWhiteSpaceSeparator); - Assert.IsFalse(parser.GetArgument("Other").AllowMultiValueWhiteSpaceSeparator); - - var result = parser.Parse(new[] { "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 = parser.Parse(new[] { "-Multi", "1", "-Multi", "2" }); - CollectionAssert.AreEqual(new[] { 1, 2 }, result.Multi); - - CheckThrows(() => parser.Parse(new[] { "1", "-Multi", "-Other", "5", "6" }), parser, CommandLineArgumentErrorCategory.MissingNamedArgumentValue, "Multi"); - CheckThrows(() => parser.Parse(new[] { "-MultiSwitch", "true", "false" }), parser, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Arg1", ArgumentConversionInner); - parser.Options.AllowWhiteSpaceValueSeparator = false; - CheckThrows(() => parser.Parse(new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }), parser, CommandLineArgumentErrorCategory.TooManyArguments); + Assert.AreEqual(0, result.Arg12.Count); } - - [TestMethod] - public void TestInjection() + else { - var parser = new CommandLineParser(); - var result = parser.Parse(new[] { "-Arg", "1" }); - Assert.AreSame(parser, result.Parser); - Assert.AreEqual(1, result.Arg); - - var parser2 = new CommandLineParser(); - var result2 = parser2.Parse(new[] { "-Arg1", "1", "-Arg2", "2", "-Arg3", "3" }); - Assert.AreSame(parser2, result2.Parser); - Assert.AreEqual(1, result2.Arg1); - Assert.AreEqual(2, result2.Arg2); - Assert.AreEqual(3, result2.Arg3); + CollectionAssert.AreEqual(arg12, result.Arg12); } - [TestMethod] - public void TestDuplicateArguments() + CollectionAssert.AreEqual(arg13, result.Arg13); + if (arg14 == null) { - var parser = new CommandLineParser(); - CheckThrows(() => parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }), parser, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1"); - parser.Options.DuplicateArguments = ErrorMode.Allow; - var result = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); - Assert.AreEqual("bar", result.Argument1); - - bool handlerCalled = false; - bool keepOldValue = false; - EventHandler handler = (sender, e) => - { - Assert.AreEqual("Argument1", e.Argument.ArgumentName); - Assert.AreEqual("foo", e.Argument.Value); - Assert.AreEqual("bar", e.NewValue); - handlerCalled = true; - if (keepOldValue) - { - e.KeepOldValue = true; - } - }; - - parser.DuplicateArgument += handler; - - // Handler is not called when duplicates not allowed. - parser.Options.DuplicateArguments = ErrorMode.Error; - CheckThrows(() => parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }), parser, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1"); - Assert.IsFalse(handlerCalled); - - // Now it is called. - parser.Options.DuplicateArguments = ErrorMode.Allow; - result = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); - Assert.AreEqual("bar", result.Argument1); - Assert.IsTrue(handlerCalled); - - // Also called for warning, and keep the old value. - parser.Options.DuplicateArguments = ErrorMode.Warning; - handlerCalled = false; - keepOldValue = true; - result = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); - Assert.AreEqual("foo", result.Argument1); - Assert.IsTrue(handlerCalled); + Assert.AreEqual(0, result.Arg14.Count); } - - [TestMethod] - public void TestConversion() + else { - var parser = new CommandLineParser(); - var result = parser.Parse("-ParseCulture 1 -Parse 2 -Ctor 3 -ParseNullable 4 -ParseMulti 5 6 -ParseNullableMulti 7 8 -NullableMulti 9 10 -Nullable 11".Split(' ')); - Assert.AreEqual(1, result.ParseCulture.Value); - Assert.AreEqual(2, result.Parse.Value); - Assert.AreEqual(3, result.Ctor.Value); - Assert.AreEqual(4, result.ParseNullable.Value.Value); - Assert.AreEqual(5, result.ParseMulti[0].Value); - Assert.AreEqual(6, result.ParseMulti[1].Value); - Assert.AreEqual(7, result.ParseNullableMulti[0].Value.Value); - Assert.AreEqual(8, result.ParseNullableMulti[1].Value.Value); - Assert.AreEqual(9, result.NullableMulti[0].Value); - Assert.AreEqual(10, result.NullableMulti[1].Value); - Assert.AreEqual(11, result.Nullable); - - result = parser.Parse(new[] { "-ParseNullable", "", "-NullableMulti", "1", "", "2", "-ParseNullableMulti", "3", "", "4" }); - Assert.IsNull(result.ParseNullable); - Assert.AreEqual(1, result.NullableMulti[0].Value); - Assert.IsNull(result.NullableMulti[1]); - Assert.AreEqual(2, result.NullableMulti[2].Value); - Assert.AreEqual(3, result.ParseNullableMulti[0].Value.Value); - Assert.IsNull(result.ParseNullableMulti[1]); - Assert.AreEqual(4, result.ParseNullableMulti[2].Value.Value); + CollectionAssert.AreEqual(arg14, (System.Collections.ICollection)result.Arg14); } - private class ExpectedArgument + if (arg15 == null) { - public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) - { - Name = name; - Type = type; - Kind = kind; - } - - public string Name { get; set; } - public string MemberName { get; set; } - public Type Type { get; set; } - public Type ElementType { get; set; } - public int? Position { get; set; } - public bool IsRequired { get; set; } - public object DefaultValue { get; set; } - public string Description { get; set; } - public string ValueDescription { get; set; } - public bool IsSwitch { get; set; } - public ArgumentKind Kind { get; set; } - public string[] Aliases { get; set; } - public char? ShortName { get; set; } - public char[] ShortAliases { get; set; } - public bool IsHidden { get; set; } + Assert.AreEqual(default, result.Arg15); } - - private static void TestArgument(CommandLineArgument argument, ExpectedArgument expected) + else { - Assert.AreEqual(expected.Name, argument.ArgumentName); - Assert.AreEqual(expected.MemberName ?? expected.Name, argument.MemberName); - Assert.AreEqual(expected.ShortName.HasValue, argument.HasShortName); - Assert.AreEqual(expected.ShortName ?? '\0', argument.ShortName); - Assert.AreEqual(expected.Type, argument.ArgumentType); - Assert.AreEqual(expected.ElementType ?? expected.Type, argument.ElementType); - Assert.AreEqual(expected.Position, argument.Position); - Assert.AreEqual(expected.IsRequired, argument.IsRequired); - Assert.AreEqual(expected.Description ?? string.Empty, argument.Description); - Assert.AreEqual(expected.ValueDescription ?? argument.ElementType.Name, argument.ValueDescription); - Assert.AreEqual(expected.Kind, argument.Kind); - Assert.AreEqual(expected.Kind == ArgumentKind.MultiValue || expected.Kind == ArgumentKind.Dictionary, argument.IsMultiValue); - Assert.AreEqual(expected.Kind == ArgumentKind.Dictionary, argument.IsDictionary); - Assert.AreEqual(expected.IsSwitch, argument.IsSwitch); - Assert.AreEqual(expected.DefaultValue, argument.DefaultValue); - Assert.AreEqual(expected.IsHidden, argument.IsHidden); - Assert.IsFalse(argument.AllowMultiValueWhiteSpaceSeparator); - Assert.IsNull(argument.Value); - Assert.IsFalse(argument.HasValue); - CollectionAssert.AreEqual(expected.Aliases, argument.Aliases); - CollectionAssert.AreEqual(expected.ShortAliases, argument.ShortAliases); + Assert.AreEqual(arg15.Value, result.Arg15); } + } - private static void TestArguments(IEnumerable arguments, ExpectedArgument[] expected) + private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string? argumentName = null, Type? innerExceptionType = null, int remainingArgumentCount = 0) + { + try { - int index = 0; - foreach (var arg in arguments) - { - Assert.IsTrue(index < expected.Length, "Too many arguments."); - TestArgument(arg, expected[index]); - ++index; - } + parser.Parse(arguments); + Assert.Fail("Expected CommandLineException was not thrown."); } - - private static void TestParse(CommandLineParser target, string commandLine, string arg1 = null, int arg2 = 42, bool notSwitch = false, string arg3 = null, int arg4 = 47, float arg5 = 0.0f, string arg6 = null, bool arg7 = false, DayOfWeek[] arg8 = null, int? arg9 = null, bool[] arg10 = null, bool? arg11 = null, int[] arg12 = null, Dictionary arg13 = null, Dictionary arg14 = null, KeyValuePair? arg15 = null) + catch (CommandLineArgumentException ex) { - string[] args = commandLine.Split(' '); // not using quoted arguments in the tests, so this is fine. - var result = target.Parse(args); - Assert.IsNotNull(result); - Assert.AreEqual(ParseStatus.Success, target.ParseResult.Status); - Assert.IsNull(target.ParseResult.LastException); - Assert.IsNull(target.ParseResult.ArgumentName); - Assert.IsFalse(target.HelpRequested); - Assert.AreEqual(arg1, result.Arg1); - Assert.AreEqual(arg2, result.Arg2); - Assert.AreEqual(arg3, result.Arg3); - Assert.AreEqual(arg4, result.Arg4); - Assert.AreEqual(arg5, result.Arg5); - Assert.AreEqual(arg6, result.Arg6); - Assert.AreEqual(arg7, result.Arg7); - CollectionAssert.AreEqual(arg8, result.Arg8); - Assert.AreEqual(arg9, result.Arg9); - CollectionAssert.AreEqual(arg10, result.Arg10); - Assert.AreEqual(arg11, result.Arg11); - Assert.AreEqual(notSwitch, result.NotSwitch); - if (arg12 == null) - { - Assert.AreEqual(0, result.Arg12.Count); - } - else - { - CollectionAssert.AreEqual(arg12, result.Arg12); - } - - CollectionAssert.AreEqual(arg13, result.Arg13); - if (arg14 == null) + Assert.IsTrue(parser.HelpRequested); + Assert.AreEqual(ParseStatus.Error, parser.ParseResult.Status); + Assert.AreEqual(ex, parser.ParseResult.LastException); + Assert.AreEqual(ex.ArgumentName, parser.ParseResult.LastException!.ArgumentName); + Assert.AreEqual(category, ex.Category); + Assert.AreEqual(argumentName, ex.ArgumentName); + if (innerExceptionType == null) { - Assert.AreEqual(0, result.Arg14.Count); + Assert.IsNull(ex.InnerException); } else { - CollectionAssert.AreEqual(arg14, (System.Collections.ICollection)result.Arg14); + Assert.IsInstanceOfType(ex.InnerException, innerExceptionType); } - if (arg15 == null) - { - Assert.AreEqual(default(KeyValuePair), result.Arg15); - } - else - { - Assert.AreEqual(arg15.Value, result.Arg15); - } + var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); + AssertMemoryEqual(remaining, parser.ParseResult.RemainingArguments); } + } + + private static void CheckCanceled(CommandLineParser parser, string[] arguments, string argumentName, bool helpRequested, int remainingArgumentCount = 0) + { + Assert.IsNull(parser.Parse(arguments)); + Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); + Assert.AreEqual(argumentName, parser.ParseResult.ArgumentName); + Assert.AreEqual(helpRequested, parser.HelpRequested); + Assert.IsNull(parser.ParseResult.LastException); + var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); + AssertMemoryEqual(remaining, parser.ParseResult.RemainingArguments); + } - private static void CheckThrows(Action operation, CommandLineParser parser, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null) + private static T CheckSuccess(CommandLineParser parser, string[] arguments, string? argumentName = null, int remainingArgumentCount = 0) + where T : class + { + var result = parser.Parse(arguments); + Assert.IsNotNull(result); + Assert.IsFalse(parser.HelpRequested); + Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); + Assert.AreEqual(argumentName, parser.ParseResult.ArgumentName); + Assert.IsNull(parser.ParseResult.LastException); + var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); + AssertMemoryEqual(remaining, parser.ParseResult.RemainingArguments); + return result; + } + + internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions? options = null) +#if NET7_0_OR_GREATER + where T : class, IParserProvider +#else + where T : class +#endif + { + var parser = kind switch { - try - { - operation(); - Assert.Fail("Expected CommandLineException was not thrown."); - } - catch (CommandLineArgumentException ex) - { - Assert.IsTrue(parser.HelpRequested); - Assert.AreEqual(ParseStatus.Error, parser.ParseResult.Status); - Assert.AreEqual(ex, parser.ParseResult.LastException); - Assert.AreEqual(ex.ArgumentName, parser.ParseResult.LastException.ArgumentName); - Assert.AreEqual(category, ex.Category); - Assert.AreEqual(argumentName, ex.ArgumentName); - if (innerExceptionType == null) - { - Assert.IsNull(ex.InnerException); - } - else - { - Assert.IsInstanceOfType(ex.InnerException, innerExceptionType); - } - } + ProviderKind.Reflection => new CommandLineParser(options), +#if NET7_0_OR_GREATER + ProviderKind.Generated => T.CreateParser(options), +#else + ProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object?[] { options })!, +#endif + _ => throw new InvalidOperationException() + }; + + Assert.AreEqual(kind, parser.ProviderKind); + return parser; + } + + private static T? StaticParse(ProviderKind kind, string[] args, ParseOptions? options = null) +#if NET7_0_OR_GREATER + where T : class, IParser +#else + where T : class +#endif + { + return kind switch + { + ProviderKind.Reflection => CommandLineParser.Parse(args, options), +#if NET7_0_OR_GREATER + ProviderKind.Generated => T.Parse(args, options), +#else + ProviderKind.Generated => (T?)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object?[] { args, options }), +#endif + _ => throw new InvalidOperationException() + }; + } + + + public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) + => $"{methodInfo.Name} ({data[0]})"; + + + public static IEnumerable ProviderKinds + => new[] + { + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } + }; + + public static void AssertSpanEqual(ReadOnlySpan expected, ReadOnlySpan actual) + where T : IEquatable + { + if (!expected.SequenceEqual(actual)) + { + Assert.Fail($"Span not equal. Expected: {{ {string.Join(", ", expected.ToArray())} }}, Actual: {{ {string.Join(", ", actual.ToArray())} }}"); } } + + public static void AssertMemoryEqual(ReadOnlyMemory expected, ReadOnlyMemory actual) + where T : IEquatable + { + AssertSpanEqual(expected.Span, actual.Span); + } } diff --git a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs new file mode 100644 index 00000000..b48abbd7 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs @@ -0,0 +1,98 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Terminal; +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class CommandOptionsTest +{ + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // In case other tests had changed this. + ParseOptions.ForceReflectionDefault = false; + } + + [TestMethod] + public void TestConstructor() + { + var options = new CommandOptions(); + // Values from ParseOptions that are the same. + Assert.IsNull(options.AllowWhiteSpaceValueSeparator); + Assert.IsNull(options.ArgumentNameComparison); + Assert.IsNull(options.ArgumentNamePrefixes); + Assert.IsNull(options.ArgumentNameTransform); + Assert.IsNull(options.AutoHelpArgument); + Assert.IsNull(options.AutoPrefixAliases); + Assert.IsNull(options.AutoVersionArgument); + Assert.AreEqual(CultureInfo.InvariantCulture, options.Culture); + Assert.IsNull(options.DefaultValueDescriptions); + Assert.IsNull(options.DuplicateArguments); + Assert.IsNull(options.Error); + Assert.AreEqual(TextFormat.ForegroundRed, options.ErrorColor); + Assert.IsFalse(options.ForceReflection); + Assert.IsFalse(options.IsPosix); + Assert.IsNull(options.LongArgumentNamePrefix); + Assert.IsNull(options.Mode); + Assert.IsNull(options.NameValueSeparators); + Assert.AreEqual(UsageHelpRequest.SyntaxOnly, options.ShowUsageOnError); + Assert.IsNotNull(options.StringProvider); + Assert.IsNotNull(options.UsageWriter); + Assert.IsNull(options.UseErrorColor); + Assert.IsNull(options.ValueDescriptionTransform); + Assert.AreEqual(TextFormat.ForegroundYellow, options.WarningColor); + + // Properties defined by CommandOptions itself + Assert.IsTrue(options.AutoCommandPrefixAliases); + Assert.IsTrue(options.AutoVersionCommand); + Assert.IsNull(options.CommandFilter); + Assert.AreEqual(StringComparison.OrdinalIgnoreCase, options.CommandNameComparison); + Assert.AreEqual(NameTransform.None, options.CommandNameTransform); + Assert.IsNull(options.ParentCommand); + Assert.AreEqual("Command", options.StripCommandNameSuffix); + } + + [TestMethod] + public void TestIsPosix() + { + var options = new CommandOptions() + { + IsPosix = true + }; + + Assert.IsTrue(options.IsPosix); + Assert.AreEqual(ParsingMode.LongShort, options.Mode); + Assert.AreEqual(StringComparison.InvariantCulture, options.ArgumentNameComparison); + Assert.AreEqual(NameTransform.DashCase, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.DashCase, options.ValueDescriptionTransform); + Assert.AreEqual(StringComparison.InvariantCulture, options.CommandNameComparison); + Assert.AreEqual(NameTransform.DashCase, options.CommandNameTransform); + options.CommandNameComparison = StringComparison.CurrentCultureIgnoreCase; + Assert.IsFalse(options.IsPosix); + options.CommandNameComparison = StringComparison.CurrentCulture; + Assert.IsTrue(options.IsPosix); + + options.IsPosix = false; + Assert.AreEqual(ParsingMode.Default, options.Mode); + Assert.AreEqual(StringComparison.OrdinalIgnoreCase, options.ArgumentNameComparison); + Assert.AreEqual(NameTransform.None, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); + Assert.AreEqual(StringComparison.OrdinalIgnoreCase, options.CommandNameComparison); + Assert.AreEqual(NameTransform.None, options.CommandNameTransform); + + options = new CommandOptions() + { + Mode = ParsingMode.LongShort, + ArgumentNameComparison = StringComparison.InvariantCulture, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase, + CommandNameComparison = StringComparison.InvariantCulture, + CommandNameTransform = NameTransform.DashCase, + }; + + Assert.IsTrue(options.IsPosix); + } +} diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 18c95e9f..9cee1d74 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -3,99 +3,155 @@ using System.ComponentModel; using System.Threading.Tasks; -namespace Ookii.CommandLine.Tests +#pragma warning disable OCL0033,OCL0034 + +namespace Ookii.CommandLine.Tests; + +[GeneratedCommandManager] +partial class GeneratedManager { } + +[GeneratedCommandManager(AssemblyNames = new[] { "Ookii.CommandLine.Tests.Commands" })] +partial class GeneratedManagerWithExplicitAssembly { } + +// Also tests using identity instead of name. +[GeneratedCommandManager(AssemblyNames = new[] { "Ookii.CommandLine.Tests", "Ookii.CommandLine.Tests.Commands, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0c15020868fd6249" })] +partial class GeneratedManagerWithMultipleAssemblies { } + +[GeneratedParser] +[Command("test")] +[Description("Test command description.")] +public partial class TestCommand : ICommand { - [Command("test")] - [Description("Test command description.")] - public class TestCommand : ICommand - { - [CommandLineArgument] - public string Argument { get; set; } + [CommandLineArgument] + public string? Argument { get; set; } - public int Run() - { - throw new NotImplementedException(); - } + public int Run() + { + throw new NotImplementedException(); } +} - [Command] - [Alias("alias")] - public class AnotherSimpleCommand : ICommand +[GeneratedParser] +[Command] +[Alias("alias")] +public partial class AnotherSimpleCommand : ICommand +{ + [CommandLineArgument] + [Description("Argument description")] + public int Value { get; set; } + + public int Run() { - [CommandLineArgument] - [Description("Argument description")] - public int Value { get; set; } - - public int Run() - { - return Value; - } + return Value; } +} - [Command("custom")] - [Description("Custom parsing command.")] - internal class CustomParsingCommand : ICommandWithCustomParsing +[Command("custom")] +[Description("Custom parsing command.")] +partial class CustomParsingCommand : ICommandWithCustomParsing +{ + public void Parse(ReadOnlyMemory args, CommandManager manager) { - public void Parse(string[] args, int index, CommandOptions options) - { - Value = args[index]; - } + Value = args.Span[0]; + } - public string Value { get; set; } + public string? Value { get; set; } - public int Run() - { - throw new NotImplementedException(); - } + public int Run() + { + throw new NotImplementedException(); } +} - [Command(IsHidden = true)] - class HiddenCommand : ICommand +[GeneratedParser] +[Command(IsHidden = true)] +partial class HiddenCommand : ICommand +{ + public int Run() { - public int Run() - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } +} - // Hidden so I don't have to update the expected usage. - [Command(IsHidden = true)] - [Description("Async command description.")] - class AsyncCommand : IAsyncCommand +// Hidden so I don't have to update the expected usage. +// Not generated to test registration of plain commands without generation. +[Command(IsHidden = true)] +[Description("Async command description.")] +partial class AsyncCommand : IAsyncCommand +{ + [CommandLineArgument(Position = 0)] + [Description("Argument description.")] + public int Value { get; set; } + + public int Run() { - [CommandLineArgument(Position = 0)] - [Description("Argument description.")] - public int Value { get; set; } - - public int Run() - { - // Do somehting different than RunAsync so the test can differentiate which one was - // called. - return Value + 1; - } - - public Task RunAsync() - { - return Task.FromResult(Value); - } + // Do something different than RunAsync so the test can differentiate which one was + // called. + return Value + 1; } - // Used in stand-alone test, so not an actual command. - class AsyncBaseCommand : AsyncCommandBase + public Task RunAsync() { - public override async Task RunAsync() - { - // Do something actually async to test the wait in Run(). - await Task.Delay(100); - return 42; - } + return Task.FromResult(Value); } +} - public class NotACommand : ICommand +// Used in stand-alone test, so not an actual command. +class AsyncBaseCommand : AsyncCommandBase +{ + public override async Task RunAsync() { - public int Run() - { - throw new NotImplementedException(); - } + // Do something actually async to test the wait in Run(). + await Task.Yield(); + return 42; } } + +public class NotACommand : ICommand +{ + public int Run() + { + throw new NotImplementedException(); + } +} + +[Command(IsHidden = true)] +[Description("Parent command description.")] +class TestParentCommand : ParentCommand +{ +} + +[GeneratedParser] +[Command] +[ParentCommand(typeof(TestParentCommand))] +partial class TestChildCommand : ICommand +{ + [CommandLineArgument] + public int Value { get; set; } + + public int Run() => Value; +} + +[GeneratedParser] +[Command] +[ParentCommand(typeof(TestParentCommand))] +partial class OtherTestChildCommand : ICommand +{ + public int Run() => throw new NotImplementedException(); +} + +[Command] +[ParentCommand(typeof(TestParentCommand))] +[Description("Other parent command description.")] +class NestedParentCommand : ParentCommand +{ +} + + +[GeneratedParser] +[Command] +[ParentCommand(typeof(NestedParentCommand))] +partial class NestedParentChildCommand : ICommand +{ + public int Run() => throw new NotImplementedException(); +} diff --git a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs index 16755cb7..126995fa 100644 --- a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs +++ b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs @@ -1,39 +1,35 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Conversion; using System.Collections.Generic; +using System.Globalization; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class KeyValuePairConverterTest { - [TestClass] - public class KeyValuePairConverterTest + // Needed because SpanParsableConverter only exists on .Net 7. + private class IntConverter : ArgumentConverter { - [TestMethod] - public void TestConvertFrom() - { - var converter = new KeyValuePairConverter(); - Assert.IsTrue(converter.CanConvertFrom(null, typeof(string))); - var converted = converter.ConvertFromInvariantString(null, "foo=5"); - Assert.AreEqual(KeyValuePair.Create("foo", 5), converted); - } - - [TestMethod] - public void TestConvertTo() - { - var converter = new KeyValuePairConverter(); - Assert.IsTrue(converter.CanConvertTo(null, typeof(string))); - var converted = converter.ConvertToInvariantString(null, KeyValuePair.Create("bar", 6)); - Assert.AreEqual("bar=6", converted); - } + public override object Convert(string value, CultureInfo culture, CommandLineArgument argument) + => int.Parse(value, culture); + } - [TestMethod] - public void TestCustomSeparator() - { - var converter = new KeyValuePairConverter(new LocalizedStringProvider(), "Test", false, null, null, ":"); - var pair = converter.ConvertFromInvariantString(null, "foo:5"); - Assert.AreEqual(KeyValuePair.Create("foo", 5), pair); + [TestMethod] + public void TestConvertFrom() + { + var parser = new CommandLineParser(); + var converter = new KeyValuePairConverter(); + var converted = converter.Convert("foo=5", CultureInfo.InvariantCulture, parser.GetArgument("Argument1")!); + Assert.AreEqual(KeyValuePair.Create("foo", 5), converted); + } - Assert.IsTrue(converter.CanConvertTo(null, typeof(string))); - var converted = converter.ConvertToInvariantString(null, KeyValuePair.Create("bar", 6)); - Assert.AreEqual("bar:6", converted); - } + [TestMethod] + public void TestCustomSeparator() + { + var parser = new CommandLineParser(); + var converter = new KeyValuePairConverter(new StringConverter(), new IntConverter(), ":", false); + var pair = converter.Convert("foo:5", CultureInfo.InvariantCulture, parser.GetArgument("Argument1")!); + Assert.AreEqual(KeyValuePair.Create("foo", 5), pair); } } diff --git a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs index 520c7878..e2c5b0dd 100644 --- a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs +++ b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.Constants.cs @@ -1,8 +1,8 @@ -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +public partial class LineWrappingTextWriterTest { - public partial class LineWrappingTextWriterTest - { - private static readonly string _input = @" + private static readonly string _input = @" 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 @@ -15,7 +15,7 @@ fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae element Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789".ReplaceLineEndings(); - private static readonly string _expectedNoIndent = @" + private static readonly string _expectedNoIndent = @" 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. @@ -37,7 +37,7 @@ elementum curabitur. 0123456789012345678901234567890123456789 ".ReplaceLineEndings(); - private static readonly string _expectedIndent = @" + private static readonly string _expectedIndent = @" 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. @@ -60,7 +60,7 @@ elementum curabitur. 45678901234567890123456789012345678901234567890123456789 ".ReplaceLineEndings(); - private static readonly string _expectedIndentChanges = @" + private static readonly string _expectedIndentChanges = @" 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,7 +123,7 @@ elementum curabitur. 45678901234567890123456789012345678901234567890123456789 ".ReplaceLineEndings(); - private static readonly string _expectedIndentNoMaximum = @" + 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. Tincidunt vitae semper quis lectus nulla at volutpat diam ut. Vitae tempus @@ -136,34 +136,34 @@ fermentum et sollicitudin ac orci. Aliquam malesuada bibendum arcu vitae element Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789".ReplaceLineEndings(); - private static readonly string _inputFormatting = "\x1b[34mLorem \x1b[34mipsum \x1b[34mdolor \x1b[34msit \x1b[34mamet, \x1b[34mconsectetur \x1b[34madipiscing \x1b[34melit, \x1b]0;new title\x1b\\sed do \x1b]0;new title2\x0007eiusmod \x1b(Btempor\x1bH incididunt\nut labore et dolore magna aliqua. Donec\x1b[38;2;1;2;3m adipiscing tristique risus nec feugiat in fermentum.\x1b[0m".ReplaceLineEndings(); + private static readonly string _inputFormatting = "\x1b[34mLorem \x1b[34mipsum \x1b[34mdolor \x1b[34msit \x1b[34mamet, \x1b[34mconsectetur \x1b[34madipiscing \x1b[34melit, \x1b]0;new title\x1b\\sed do \x1b]0;new title2\x0007eiusmod \x1b(Btempor\x1bH incididunt\nut labore et dolore magna aliqua. Donec\x1b[38;2;1;2;3m adipiscing tristique risus nec feugiat in fermentum.\x1b[0m".ReplaceLineEndings(); - private static readonly string _expectedFormatting = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, ]0;new title\sed do ]0;new title2eiusmod (BtemporH + private static readonly string _expectedFormatting = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, ]0;new title\sed do ]0;new title2eiusmod (BtemporH incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. ".ReplaceLineEndings(); - private static readonly string _expectedFormattingCounted = @"Lorem ipsum dolor sit amet, consectetur + private static readonly string _expectedFormattingCounted = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, ]0;new title\sed do ]0;new title2eiusmod (BtemporH incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum. ".ReplaceLineEndings(); - private const string _inputLongFormatting = "Lorem ipsum dolor sit amet, consectetur\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum."; + private const string _inputLongFormatting = "Lorem ipsum dolor sit amet, consectetur\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m\x1b[34m adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in fermentum."; - private static readonly string _expectedLongFormatting = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + private static readonly string _expectedLongFormatting = @"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. ".ReplaceLineEndings(); - private const string _inputWrappingMode = @"Lorem ipsum dolor sit amet, + private const string _inputWrappingMode = @"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. Lorem 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; - private static readonly string _expectedWrappingMode = @"Lorem ipsum dolor sit amet, + private static readonly string _expectedWrappingMode = @"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. @@ -187,7 +187,7 @@ dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in 234567890123456789012345678901234567890123456789 ".ReplaceLineEndings(); - private static readonly string _expectedWrappingModeWrite = @"Lorem ipsum dolor sit amet, + private static readonly string _expectedWrappingModeWrite = @"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. @@ -208,7 +208,7 @@ dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in 6789012345678901234567890123456789012345678901234567890123456789012345678901 234567890123456789012345678901234567890123456789".ReplaceLineEndings(); - private static readonly string _expectedWrappingModeNoForce = @"Lorem ipsum dolor sit amet, + private static readonly string _expectedWrappingModeNoForce = @"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,5 +232,4 @@ dolore magna aliqua. Donec adipiscing tristique risus nec feugiat in 6789012345678901234567890123456789012345678901234567890123456789012345678901 234567890123456789012345678901234567890123456789".ReplaceLineEndings(); - } } diff --git a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs index 25f9551c..6ae81a9e 100644 --- a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs +++ b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs @@ -1,550 +1,548 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Terminal; using System; using System.IO; using System.Threading.Tasks; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +[TestClass()] +public partial class LineWrappingTextWriterTest { - [TestClass()] - public partial class LineWrappingTextWriterTest + [TestMethod()] + public void TestWriteCharArray() { - [TestMethod()] - public void TestWriteCharArray() - { - const int maxLength = 80; + const int maxLength = 80; - Assert.AreEqual(_expectedNoIndent, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length)); - // write it again, in pieces exactly as long as the max line length - Assert.AreEqual(_expectedNoIndent, WriteCharArray(_input.ToCharArray(), maxLength, maxLength)); - // And again, in pieces less than the max line length - Assert.AreEqual(_expectedNoIndent, WriteCharArray(_input.ToCharArray(), maxLength, 50)); - } + Assert.AreEqual(_expectedNoIndent, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length)); + // write it again, in pieces exactly as long as the max line length + Assert.AreEqual(_expectedNoIndent, WriteCharArray(_input.ToCharArray(), maxLength, maxLength)); + // And again, in pieces less than the max line length + Assert.AreEqual(_expectedNoIndent, WriteCharArray(_input.ToCharArray(), maxLength, 50)); + } - [TestMethod()] - public void TestWriteString() - { - const int maxLength = 80; + [TestMethod()] + public void TestWriteString() + { + const int maxLength = 80; - Assert.AreEqual(_expectedNoIndent, WriteString(_input, maxLength, _input.Length)); - // Write it again, in pieces exactly as long as the max line length. - Assert.AreEqual(_expectedNoIndent, WriteString(_input, maxLength, maxLength)); - // And again, in pieces less than the max line length. - Assert.AreEqual(_expectedNoIndent, WriteString(_input, maxLength, 50)); - } + Assert.AreEqual(_expectedNoIndent, WriteString(_input, maxLength, _input.Length)); + // Write it again, in pieces exactly as long as the max line length. + Assert.AreEqual(_expectedNoIndent, WriteString(_input, maxLength, maxLength)); + // And again, in pieces less than the max line length. + Assert.AreEqual(_expectedNoIndent, WriteString(_input, maxLength, 50)); + } - [TestMethod()] - public async Task TestWriteStringAsync() - { - const int maxLength = 80; + [TestMethod()] + public async Task TestWriteStringAsync() + { + const int maxLength = 80; - Assert.AreEqual(_expectedNoIndent, await WriteStringAsync(_input, maxLength, _input.Length)); - // Write it again, in pieces exactly as long as the max line length. - Assert.AreEqual(_expectedNoIndent, await WriteStringAsync(_input, maxLength, maxLength)); - // And again, in pieces less than the max line length. - Assert.AreEqual(_expectedNoIndent, await WriteStringAsync(_input, maxLength, 50)); - } + Assert.AreEqual(_expectedNoIndent, await WriteStringAsync(_input, maxLength, _input.Length)); + // Write it again, in pieces exactly as long as the max line length. + Assert.AreEqual(_expectedNoIndent, await WriteStringAsync(_input, maxLength, maxLength)); + // And again, in pieces less than the max line length. + Assert.AreEqual(_expectedNoIndent, await WriteStringAsync(_input, maxLength, 50)); + } - [TestMethod()] - public void TestWriteStringNoMaximum() - { - const int maxLength = 0; + [TestMethod()] + public void TestWriteStringNoMaximum() + { + const int maxLength = 0; - Assert.AreEqual(_input, WriteString(_input, maxLength, _input.Length)); - // Write it again, in pieces. - Assert.AreEqual(_input, WriteString(_input, maxLength, 80)); - } + Assert.AreEqual(_input, WriteString(_input, maxLength, _input.Length)); + // Write it again, in pieces. + Assert.AreEqual(_input, WriteString(_input, maxLength, 80)); + } - [TestMethod()] - public void TestWriteCharArrayNoMaximum() - { - const int maxLength = 0; + [TestMethod()] + public void TestWriteCharArrayNoMaximum() + { + const int maxLength = 0; - Assert.AreEqual(_input, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length)); - // Write it again, in pieces. - Assert.AreEqual(_input, WriteCharArray(_input.ToCharArray(), maxLength, 80)); - } + Assert.AreEqual(_input, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length)); + // Write it again, in pieces. + Assert.AreEqual(_input, WriteCharArray(_input.ToCharArray(), maxLength, 80)); + } - [TestMethod()] - public void TestWriteUnixLineEnding() - { - const int maxLength = 80; - var input = _input.ReplaceLineEndings("\n"); - Assert.AreEqual(_expectedNoIndent, WriteString(input, maxLength, input.Length)); - - using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); - writer.NewLine = "\n"; - var expected = _expectedNoIndent.ReplaceLineEndings("\n"); - Assert.AreEqual(expected, WriteString(writer, input, input.Length)); - } + [TestMethod()] + public void TestWriteUnixLineEnding() + { + const int maxLength = 80; + var input = _input.ReplaceLineEndings("\n"); + Assert.AreEqual(_expectedNoIndent, WriteString(input, maxLength, input.Length)); + + using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); + writer.NewLine = "\n"; + var expected = _expectedNoIndent.ReplaceLineEndings("\n"); + Assert.AreEqual(expected, WriteString(writer, input, input.Length)); + } - [TestMethod()] - public void TestWriteWindowsLineEnding() - { - const int maxLength = 80; - var input = _input.ReplaceLineEndings("\r\n"); - Assert.AreEqual(_expectedNoIndent, WriteString(input, maxLength, input.Length)); - - using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); - writer.NewLine = "\r\n"; - var expected = _expectedNoIndent.ReplaceLineEndings("\r\n"); - Assert.AreEqual(expected, WriteString(writer, input, input.Length)); - } + [TestMethod()] + public void TestWriteWindowsLineEnding() + { + const int maxLength = 80; + var input = _input.ReplaceLineEndings("\r\n"); + Assert.AreEqual(_expectedNoIndent, WriteString(input, maxLength, input.Length)); + + using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); + writer.NewLine = "\r\n"; + var expected = _expectedNoIndent.ReplaceLineEndings("\r\n"); + Assert.AreEqual(expected, WriteString(writer, input, input.Length)); + } - [TestMethod()] - public void TestIndentString() - { - const int maxLength = 80; - const int indent = 8; - - Assert.AreEqual(_expectedIndent, WriteString(_input, maxLength, _input.Length, indent)); - // Write it again, in pieces exactly as long as the max line length. - Assert.AreEqual(_expectedIndent, WriteString(_input, maxLength, maxLength, indent)); - // And again, in pieces less than the max line length. - Assert.AreEqual(_expectedIndent, WriteString(_input, maxLength, 50, indent)); - } + [TestMethod()] + public void TestIndentString() + { + const int maxLength = 80; + const int indent = 8; + + Assert.AreEqual(_expectedIndent, WriteString(_input, maxLength, _input.Length, indent)); + // Write it again, in pieces exactly as long as the max line length. + Assert.AreEqual(_expectedIndent, WriteString(_input, maxLength, maxLength, indent)); + // And again, in pieces less than the max line length. + Assert.AreEqual(_expectedIndent, WriteString(_input, maxLength, 50, indent)); + } - [TestMethod()] - public void TestIndentCharArray() - { - const int maxLength = 80; - const int indent = 8; - - Assert.AreEqual(_expectedIndent, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length, indent)); - // Write it again, in pieces exactly as long as the max line length. - Assert.AreEqual(_expectedIndent, WriteCharArray(_input.ToCharArray(), maxLength, maxLength, indent)); - // And again, in pieces less than the max line length. - Assert.AreEqual(_expectedIndent, WriteCharArray(_input.ToCharArray(), maxLength, 50, indent)); - } + [TestMethod()] + public void TestIndentCharArray() + { + const int maxLength = 80; + const int indent = 8; + + Assert.AreEqual(_expectedIndent, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length, indent)); + // Write it again, in pieces exactly as long as the max line length. + Assert.AreEqual(_expectedIndent, WriteCharArray(_input.ToCharArray(), maxLength, maxLength, indent)); + // And again, in pieces less than the max line length. + Assert.AreEqual(_expectedIndent, WriteCharArray(_input.ToCharArray(), maxLength, 50, indent)); + } - [TestMethod()] - public void TestIndentChanges() - { - using var writer = LineWrappingTextWriter.ForStringWriter(80); - 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(_expectedIndentChanges, writer.BaseWriter.ToString()); - } + [TestMethod()] + public void TestIndentChanges() + { + using var writer = LineWrappingTextWriter.ForStringWriter(80); + 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(_expectedIndentChanges, writer.BaseWriter.ToString()); + } - [TestMethod()] - public async Task TestIndentChangesAsync() - { - using var writer = LineWrappingTextWriter.ForStringWriter(80); - writer.Indent = 4; - await writer.WriteLineAsync(_input); - writer.Indent = 8; - await writer.WriteLineAsync(_input.Trim()); - // Should add a new line. - await writer.ResetIndentAsync(); - await writer.WriteLineAsync(_input.Trim()); - // Should not add a new line. - await writer.ResetIndentAsync(); - await writer.FlushAsync(); - - Assert.AreEqual(_expectedIndentChanges, writer.BaseWriter.ToString()); - } + [TestMethod()] + public async Task TestIndentChangesAsync() + { + using var writer = LineWrappingTextWriter.ForStringWriter(80); + writer.Indent = 4; + await writer.WriteLineAsync(_input); + writer.Indent = 8; + await writer.WriteLineAsync(_input.Trim()); + // Should add a new line. + await writer.ResetIndentAsync(); + await writer.WriteLineAsync(_input.Trim()); + // Should not add a new line. + await writer.ResetIndentAsync(); + await writer.FlushAsync(); + + Assert.AreEqual(_expectedIndentChanges, writer.BaseWriter.ToString()); + } - [TestMethod()] - public void TestIndentStringNoMaximum() - { - const int maxLength = 0; - const int indent = 8; + [TestMethod()] + public void TestIndentStringNoMaximum() + { + const int maxLength = 0; + const int indent = 8; - Assert.AreEqual(_expectedIndentNoMaximum, WriteString(_input, maxLength, _input.Length, indent)); - // Write it again, in pieces. - Assert.AreEqual(_expectedIndentNoMaximum, WriteString(_input, maxLength, 80, indent)); - } + Assert.AreEqual(_expectedIndentNoMaximum, WriteString(_input, maxLength, _input.Length, indent)); + // Write it again, in pieces. + Assert.AreEqual(_expectedIndentNoMaximum, WriteString(_input, maxLength, 80, indent)); + } - [TestMethod()] - public void TestIndentCharArrayNoMaximum() - { - const int maxLength = 0; - const int indent = 8; + [TestMethod()] + public void TestIndentCharArrayNoMaximum() + { + const int maxLength = 0; + const int indent = 8; - Assert.AreEqual(_expectedIndentNoMaximum, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length, indent)); - // Write it again, in pieces. - Assert.AreEqual(_expectedIndentNoMaximum, WriteCharArray(_input.ToCharArray(), maxLength, 80, indent)); - } + Assert.AreEqual(_expectedIndentNoMaximum, WriteCharArray(_input.ToCharArray(), maxLength, _input.Length, indent)); + // Write it again, in pieces. + Assert.AreEqual(_expectedIndentNoMaximum, WriteCharArray(_input.ToCharArray(), maxLength, 80, indent)); + } - /// - ///A test for LineWrappingTextWriter Constructor - /// - [TestMethod()] - public void TestConstructor() - { - int maximumLineLength = 85; - bool disposeBaseWriter = true; - using TextWriter baseWriter = new StringWriter(); - using LineWrappingTextWriter target = new LineWrappingTextWriter(baseWriter, maximumLineLength, disposeBaseWriter); - Assert.AreEqual(baseWriter, target.BaseWriter); - Assert.AreEqual(maximumLineLength, target.MaximumLineLength); - Assert.AreEqual(0, target.Indent); - Assert.AreEqual(baseWriter.Encoding, target.Encoding); - Assert.AreEqual(baseWriter.FormatProvider, target.FormatProvider); - Assert.AreEqual(baseWriter.NewLine, target.NewLine); - Assert.AreEqual(WrappingMode.Enabled, target.Wrapping); - } + /// + ///A test for LineWrappingTextWriter Constructor + /// + [TestMethod()] + public void TestConstructor() + { + int maximumLineLength = 85; + bool disposeBaseWriter = true; + using TextWriter baseWriter = new StringWriter(); + using LineWrappingTextWriter target = new LineWrappingTextWriter(baseWriter, maximumLineLength, disposeBaseWriter); + Assert.AreEqual(baseWriter, target.BaseWriter); + Assert.AreEqual(maximumLineLength, target.MaximumLineLength); + Assert.AreEqual(0, target.Indent); + Assert.AreEqual(baseWriter.Encoding, target.Encoding); + Assert.AreEqual(baseWriter.FormatProvider, target.FormatProvider); + Assert.AreEqual(baseWriter.NewLine, target.NewLine); + Assert.AreEqual(WrappingMode.Enabled, target.Wrapping); + } - [TestMethod()] - [ExpectedException(typeof(ArgumentNullException))] - public void ConstructorTestBaseWriterNull() - { - new LineWrappingTextWriter(null, 0, false); - } + [TestMethod()] + [ExpectedException(typeof(ArgumentNullException))] + public void ConstructorTestBaseWriterNull() + { + new LineWrappingTextWriter(null!, 0, false); + } - [TestMethod()] - public void TestDisposeBaseWriterTrue() + [TestMethod()] + public void TestDisposeBaseWriterTrue() + { + using (TextWriter baseWriter = new StringWriter()) { - using (TextWriter baseWriter = new StringWriter()) + using (LineWrappingTextWriter target = new LineWrappingTextWriter(baseWriter, 80, true)) { - using (LineWrappingTextWriter target = new LineWrappingTextWriter(baseWriter, 80, true)) - { - target.Write("test"); - } - - try - { - baseWriter.Write("foo"); - Assert.Fail("base writer not disposed"); - } - catch (ObjectDisposedException) - { - } - - Assert.AreEqual("test\n".ReplaceLineEndings(), baseWriter.ToString()); + target.Write("test"); } - } - [TestMethod] - public void TestDisposeBaseWriterFalse() - { - using (TextWriter baseWriter = new StringWriter()) + try { - using (LineWrappingTextWriter target = new LineWrappingTextWriter(baseWriter, 80, false)) - { - target.Write("test"); - } - - // This will throw if the base writer was disposed. baseWriter.Write("foo"); - - Assert.AreEqual("test\nfoo".ReplaceLineEndings(), baseWriter.ToString()); + Assert.Fail("base writer not disposed"); } - } - - [TestMethod] - [ExpectedException(typeof(ArgumentOutOfRangeException))] - public void TestIndentTooSmall() - { - using (LineWrappingTextWriter target = LineWrappingTextWriter.ForStringWriter(80)) + catch (ObjectDisposedException) { - target.Indent = -1; } + + Assert.AreEqual("test\n".ReplaceLineEndings(), baseWriter.ToString()); } + } - [TestMethod] - [ExpectedException(typeof(ArgumentOutOfRangeException))] - public void TestIndentTooLarge() + [TestMethod] + public void TestDisposeBaseWriterFalse() + { + using (TextWriter baseWriter = new StringWriter()) { - using (LineWrappingTextWriter target = LineWrappingTextWriter.ForStringWriter(80)) + using (LineWrappingTextWriter target = new LineWrappingTextWriter(baseWriter, 80, false)) { - target.Indent = target.MaximumLineLength; + target.Write("test"); } - } - [TestMethod] - public void TestSkipFormatting() - { - Assert.AreEqual(_expectedFormatting, WriteString(_inputFormatting, 80, _inputFormatting.Length, 8)); - Assert.AreEqual(_expectedLongFormatting, WriteString(_inputLongFormatting, 80, _inputLongFormatting.Length, 8)); - Assert.AreEqual(_expectedLongFormatting, WriteString(_inputLongFormatting, 80, 80, 8)); - Assert.AreEqual(_expectedLongFormatting, WriteString(_inputLongFormatting, 80, 50, 8)); - Assert.AreEqual(_expectedLongFormatting, WriteChars(_inputLongFormatting.ToCharArray(), 80, 8)); - } + // This will throw if the base writer was disposed. + baseWriter.Write("foo"); - [TestMethod] - public void TestSkipFormattingNoMaximum() - { - Assert.AreEqual(_inputFormatting.ReplaceLineEndings(), WriteString(_inputFormatting, 0, _inputFormatting.Length, 0)); + Assert.AreEqual("test\nfoo".ReplaceLineEndings(), baseWriter.ToString()); } + } - [TestMethod] - public void TestCountFormatting() + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void TestIndentTooSmall() + { + using (LineWrappingTextWriter target = LineWrappingTextWriter.ForStringWriter(80)) { - using var writer = LineWrappingTextWriter.ForStringWriter(80, null, true); - writer.Indent = 8; - Assert.AreEqual(_expectedFormattingCounted, WriteString(writer, _inputFormatting, _inputFormatting.Length)); + target.Indent = -1; } + } - [TestMethod] - public void TestSplitFormatting() + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void TestIndentTooLarge() + { + using (LineWrappingTextWriter target = LineWrappingTextWriter.ForStringWriter(80)) { - using var writer = LineWrappingTextWriter.ForStringWriter(14); - writer.Write("Hello \x1b[38;2"); - writer.Write(";1;2"); - writer.Write(";3mWorld and stuff Bye\r"); - writer.Write("\nEveryone"); - writer.Flush(); - string expected = "Hello \x1b[38;2;1;2;3mWorld\nand stuff Bye\nEveryone\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); + target.Indent = target.MaximumLineLength; } + } - [TestMethod] - public void TestSplitLineBreak() - { - using var writer = LineWrappingTextWriter.ForStringWriter(80); - writer.Indent = 4; - writer.Write("Foo\r"); - writer.Write("Bar\r"); - writer.Write("\nBaz\r"); - writer.Write("\rOne\r"); - writer.Write("\r\nTwo\r\n"); - string expected = "Foo\n Bar\n Baz\n\nOne\n\nTwo\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - } + [TestMethod] + public void TestSkipFormatting() + { + Assert.AreEqual(_expectedFormatting, WriteString(_inputFormatting, 80, _inputFormatting.Length, 8)); + Assert.AreEqual(_expectedLongFormatting, WriteString(_inputLongFormatting, 80, _inputLongFormatting.Length, 8)); + Assert.AreEqual(_expectedLongFormatting, WriteString(_inputLongFormatting, 80, 80, 8)); + Assert.AreEqual(_expectedLongFormatting, WriteString(_inputLongFormatting, 80, 50, 8)); + Assert.AreEqual(_expectedLongFormatting, WriteChars(_inputLongFormatting.ToCharArray(), 80, 8)); + } - [TestMethod] - public void TestSplitLineBreakNoMaximum() - { - using var writer = LineWrappingTextWriter.ForStringWriter(); - writer.Indent = 4; - writer.Write("Foo\r"); - writer.Write("Bar\r"); - writer.Write("\nBaz\r"); - writer.Write("\rOne\r"); - writer.Write("\r\nTwo\r\n"); - string expected = "Foo\n Bar\n Baz\n\nOne\n\nTwo\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - } + [TestMethod] + public void TestSkipFormattingNoMaximum() + { + Assert.AreEqual(_inputFormatting.ReplaceLineEndings(), WriteString(_inputFormatting, 0, _inputFormatting.Length, 0)); + } - [TestMethod] - public void TestWriteChar() - { - Assert.AreEqual(_expectedIndent, WriteChars(_input.ToCharArray(), 80, 8)); - } + [TestMethod] + public void TestCountFormatting() + { + using var writer = LineWrappingTextWriter.ForStringWriter(80, null, true); + writer.Indent = 8; + Assert.AreEqual(_expectedFormattingCounted, WriteString(writer, _inputFormatting, _inputFormatting.Length)); + } - [TestMethod] - public void TestWriteCharFormatting() - { - Assert.AreEqual(_expectedFormatting, WriteChars(_inputFormatting.ToCharArray(), 80, 8)); - } + [TestMethod] + public void TestSplitFormatting() + { + using var writer = LineWrappingTextWriter.ForStringWriter(14); + writer.Write("Hello \x1b[38;2"); + writer.Write(";1;2"); + writer.Write(";3mWorld and stuff Bye\r"); + writer.Write("\nEveryone"); + writer.Flush(); + string expected = "Hello \x1b[38;2;1;2;3mWorld\nand stuff Bye\nEveryone\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + } - [TestMethod] - public void TestFlush() - { - using var writer = LineWrappingTextWriter.ForStringWriter(40); - writer.Write(TextFormat.ForegroundBlue); - writer.WriteLine("This is a test"); - writer.Write(TextFormat.Default); - writer.Flush(); - - var expected = $"{TextFormat.ForegroundBlue}This is a test\n{TextFormat.Default}\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - } + [TestMethod] + public void TestSplitLineBreak() + { + using var writer = LineWrappingTextWriter.ForStringWriter(80); + writer.Indent = 4; + writer.Write("Foo\r"); + writer.Write("Bar\r"); + writer.Write("\nBaz\r"); + writer.Write("\rOne\r"); + writer.Write("\r\nTwo\r\n"); + string expected = "Foo\n Bar\n Baz\n\nOne\n\nTwo\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + } - [TestMethod] - public void TestFlushNoNewLine() - { - using var writer = LineWrappingTextWriter.ForStringWriter(40); - writer.Indent = 4; - writer.WriteLine("This is a test"); - writer.Write("Unfinished second line"); - writer.Flush(false); - - var expected = "This is a test\n Unfinished second line".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - - writer.Write("more text"); - writer.Flush(false); - expected = "This is a test\n Unfinished second linemore text".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - writer.WriteLine(); - writer.WriteLine("Another line"); - writer.WriteLine("And another"); - expected = "This is a test\n Unfinished second linemore text\nAnother line\n And another\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - } + [TestMethod] + public void TestSplitLineBreakNoMaximum() + { + using var writer = LineWrappingTextWriter.ForStringWriter(); + writer.Indent = 4; + writer.Write("Foo\r"); + writer.Write("Bar\r"); + writer.Write("\nBaz\r"); + writer.Write("\rOne\r"); + writer.Write("\r\nTwo\r\n"); + string expected = "Foo\n Bar\n Baz\n\nOne\n\nTwo\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + } - [TestMethod] - public void TestResetIndent() - { - using var writer = LineWrappingTextWriter.ForStringWriter(40); - writer.Write(TextFormat.ForegroundBlue); - writer.WriteLine("This is a test"); - writer.Write(TextFormat.Default); - writer.ResetIndent(); - writer.WriteLine("Hello"); - - var expected = $"{TextFormat.ForegroundBlue}This is a test\n{TextFormat.Default}Hello\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - } + [TestMethod] + public void TestWriteChar() + { + Assert.AreEqual(_expectedIndent, WriteChars(_input.ToCharArray(), 80, 8)); + } - [TestMethod] - public void TestToString() - { - using var writer = LineWrappingTextWriter.ForStringWriter(40); - writer.WriteLine("This is a test"); - writer.Write("Unfinished second\x1b[34m line\x1b[0m"); - var expected = "This is a test\nUnfinished second\x1b[34m line\x1b[0m".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.ToString()); - expected = "This is a test\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.BaseWriter.ToString()); - - using var writer2 = LineWrappingTextWriter.ForConsoleOut(); - Assert.AreEqual(typeof(LineWrappingTextWriter).FullName, writer2.ToString()); - } + [TestMethod] + public void TestWriteCharFormatting() + { + Assert.AreEqual(_expectedFormatting, WriteChars(_inputFormatting.ToCharArray(), 80, 8)); + } - [TestMethod] - public void TestWrappingMode() - { - { - using var writer = LineWrappingTextWriter.ForStringWriter(80); - writer.Indent = 4; - writer.WriteLine(_inputWrappingMode); - writer.Wrapping = WrappingMode.Disabled; - writer.WriteLine(_inputWrappingMode); - writer.Wrapping = WrappingMode.Enabled; - writer.WriteLine(_inputWrappingMode); - Assert.AreEqual(_expectedWrappingMode, writer.ToString()); - } + [TestMethod] + public void TestFlush() + { + using var writer = LineWrappingTextWriter.ForStringWriter(40); + writer.Write(TextFormat.ForegroundBlue); + writer.WriteLine("This is a test"); + writer.Write(TextFormat.Default); + writer.Flush(); + + var expected = $"{TextFormat.ForegroundBlue}This is a test\n{TextFormat.Default}\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + } - // Make sure the buffer is cleared if not empty. - { - using var writer = LineWrappingTextWriter.ForStringWriter(80); - writer.Indent = 4; - writer.Write(_inputWrappingMode); - writer.Wrapping = WrappingMode.Disabled; - writer.Write(_inputWrappingMode); - writer.Wrapping = WrappingMode.Enabled; - writer.Write(_inputWrappingMode); - Assert.AreEqual(_expectedWrappingModeWrite, writer.ToString()); - } + [TestMethod] + public void TestFlushNoNewLine() + { + using var writer = LineWrappingTextWriter.ForStringWriter(40); + writer.Indent = 4; + writer.WriteLine("This is a test"); + writer.Write("Unfinished second line"); + writer.Flush(false); + + var expected = "This is a test\n Unfinished second line".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + + writer.Write("more text"); + writer.Flush(false); + expected = "This is a test\n Unfinished second linemore text".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + writer.WriteLine(); + writer.WriteLine("Another line"); + writer.WriteLine("And another"); + expected = "This is a test\n Unfinished second linemore text\nAnother line\n And another\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + } - // Test EnabledNoForce - { - using var writer = LineWrappingTextWriter.ForStringWriter(80); - writer.Indent = 4; - writer.Wrapping = WrappingMode.EnabledNoForce; - writer.Write(_inputWrappingMode); - writer.Write(_inputWrappingMode); - writer.Wrapping = WrappingMode.Enabled; - writer.Write(_inputWrappingMode); - Assert.AreEqual(_expectedWrappingModeNoForce, writer.ToString()); - } + [TestMethod] + public void TestResetIndent() + { + using var writer = LineWrappingTextWriter.ForStringWriter(40); + writer.Write(TextFormat.ForegroundBlue); + writer.WriteLine("This is a test"); + writer.Write(TextFormat.Default); + writer.ResetIndent(); + writer.WriteLine("Hello"); + + var expected = $"{TextFormat.ForegroundBlue}This is a test\n{TextFormat.Default}Hello\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + } - // Should be false and unchangeable if no maximum length. - { - using var writer = LineWrappingTextWriter.ForStringWriter(); - Assert.AreEqual(WrappingMode.Disabled, writer.Wrapping); - writer.Wrapping = WrappingMode.Enabled; - Assert.AreEqual(WrappingMode.Disabled, writer.Wrapping); - } + [TestMethod] + public void TestToString() + { + using var writer = LineWrappingTextWriter.ForStringWriter(40); + writer.WriteLine("This is a test"); + writer.Write("Unfinished second\x1b[34m line\x1b[0m"); + var expected = "This is a test\nUnfinished second\x1b[34m line\x1b[0m".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.ToString()); + expected = "This is a test\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.BaseWriter.ToString()); + + using var writer2 = LineWrappingTextWriter.ForConsoleOut(); + Assert.AreEqual(typeof(LineWrappingTextWriter).FullName, writer2.ToString()); + } + + [TestMethod] + public void TestWrappingMode() + { + { + using var writer = LineWrappingTextWriter.ForStringWriter(80); + writer.Indent = 4; + writer.WriteLine(_inputWrappingMode); + writer.Wrapping = WrappingMode.Disabled; + writer.WriteLine(_inputWrappingMode); + writer.Wrapping = WrappingMode.Enabled; + writer.WriteLine(_inputWrappingMode); + Assert.AreEqual(_expectedWrappingMode, writer.ToString()); } - [TestMethod] - public void TestExactLineLength() + // Make sure the buffer is cleared if not empty. { - // This tests for a situation where a line is the exact length of the ring buffer, - // but the buffer start is not zero. This can only happen if countFormatting is true - // otherwise the buffer is made larger than the line length to begin with. - using var writer = LineWrappingTextWriter.ForStringWriter(40, null, true); - writer.WriteLine("test"); - writer.Write("1234 1234 1234 1234 1234 1234 1234 12345"); - writer.Write("1234 1234 1234 1234 1234 1234 1234 12345"); - var expected = "test\n1234 1234 1234 1234 1234 1234 1234\n123451234 1234 1234 1234 1234 1234 1234\n12345".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.ToString()); + using var writer = LineWrappingTextWriter.ForStringWriter(80); + writer.Indent = 4; + writer.Write(_inputWrappingMode); + writer.Wrapping = WrappingMode.Disabled; + writer.Write(_inputWrappingMode); + writer.Wrapping = WrappingMode.Enabled; + writer.Write(_inputWrappingMode); + Assert.AreEqual(_expectedWrappingModeWrite, writer.ToString()); } - [TestMethod] - public void TestResizeWithFullBuffer() + // Test EnabledNoForce { - using var writer = LineWrappingTextWriter.ForStringWriter(40, null, true); + using var writer = LineWrappingTextWriter.ForStringWriter(80); + writer.Indent = 4; writer.Wrapping = WrappingMode.EnabledNoForce; - - // As with the above test, we want the buffer start to be non-zero. - writer.WriteLine("test"); - writer.Write("1234567890123456789012345678901234567890"); - writer.Write('1'); - writer.WriteLine(); - var expected = "test\n12345678901234567890123456789012345678901\n".ReplaceLineEndings(); - Assert.AreEqual(expected, writer.ToString()); + writer.Write(_inputWrappingMode); + writer.Write(_inputWrappingMode); + writer.Wrapping = WrappingMode.Enabled; + writer.Write(_inputWrappingMode); + Assert.AreEqual(_expectedWrappingModeNoForce, writer.ToString()); } - private static string WriteString(string value, int maxLength, int segmentSize, int indent = 0) + // Should be false and unchangeable if no maximum length. { - using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); - writer.Indent = indent; - return WriteString(writer, value, segmentSize); + using var writer = LineWrappingTextWriter.ForStringWriter(); + Assert.AreEqual(WrappingMode.Disabled, writer.Wrapping); + writer.Wrapping = WrappingMode.Enabled; + Assert.AreEqual(WrappingMode.Disabled, writer.Wrapping); } + } - private static string WriteString(LineWrappingTextWriter writer, string value, int segmentSize) - { - for (int i = 0; i < value.Length; i += segmentSize) - { - // Ignore the suggestion to use AsSpan, we want to call the string overload. - writer.Write(value.Substring(i, Math.Min(value.Length - i, segmentSize))); - } + [TestMethod] + public void TestExactLineLength() + { + // This tests for a situation where a line is the exact length of the ring buffer, + // but the buffer start is not zero. This can only happen if countFormatting is true + // otherwise the buffer is made larger than the line length to begin with. + using var writer = LineWrappingTextWriter.ForStringWriter(40, null, true); + writer.WriteLine("test"); + writer.Write("1234 1234 1234 1234 1234 1234 1234 12345"); + writer.Write("1234 1234 1234 1234 1234 1234 1234 12345"); + var expected = "test\n1234 1234 1234 1234 1234 1234 1234\n123451234 1234 1234 1234 1234 1234 1234\n12345".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.ToString()); + } - writer.Flush(); - return writer.ToString(); - } + [TestMethod] + public void TestResizeWithFullBuffer() + { + using var writer = LineWrappingTextWriter.ForStringWriter(40, null, true); + writer.Wrapping = WrappingMode.EnabledNoForce; + + // As with the above test, we want the buffer start to be non-zero. + writer.WriteLine("test"); + writer.Write("1234567890123456789012345678901234567890"); + writer.Write('1'); + writer.WriteLine(); + var expected = "test\n12345678901234567890123456789012345678901\n".ReplaceLineEndings(); + Assert.AreEqual(expected, writer.ToString()); + } + + private static string WriteString(string value, int maxLength, int segmentSize, int indent = 0) + { + using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); + writer.Indent = indent; + return WriteString(writer, value, segmentSize); + } - private static async Task WriteStringAsync(string value, int maxLength, int segmentSize, int indent = 0) + private static string WriteString(LineWrappingTextWriter writer, string value, int segmentSize) + { + for (int i = 0; i < value.Length; i += segmentSize) { - using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); - writer.Indent = indent; - return await WriteStringAsync(writer, value, segmentSize); + // Ignore the suggestion to use AsSpan, we want to call the string overload. + writer.Write(value.Substring(i, Math.Min(value.Length - i, segmentSize))); } - private static async Task WriteStringAsync(LineWrappingTextWriter writer, string value, int segmentSize) - { - for (int i = 0; i < value.Length; i += segmentSize) - { - // Ignore the suggestion to use AsSpan, we want to call the string overload. - await writer.WriteAsync(value.Substring(i, Math.Min(value.Length - i, segmentSize))); - } + writer.Flush(); + return writer.ToString()!; + } + + private static async Task WriteStringAsync(string value, int maxLength, int segmentSize, int indent = 0) + { + using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); + writer.Indent = indent; + return await WriteStringAsync(writer, value, segmentSize); + } - await writer.FlushAsync(); - return writer.ToString(); + private static async Task WriteStringAsync(LineWrappingTextWriter writer, string value, int segmentSize) + { + for (int i = 0; i < value.Length; i += segmentSize) + { + // Ignore the suggestion to use AsSpan, we want to call the string overload. + await writer.WriteAsync(value.Substring(i, Math.Min(value.Length - i, segmentSize))); } + await writer.FlushAsync(); + return writer.ToString()!; + } - private static string WriteCharArray(char[] value, int maxLength, int segmentSize, int indent = 0) - { - using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); - writer.Indent = indent; - for (int i = 0; i < value.Length; i += segmentSize) - { - writer.Write(value, i, Math.Min(value.Length - i, segmentSize)); - } - writer.Flush(); - return writer.BaseWriter.ToString(); + private static string WriteCharArray(char[] value, int maxLength, int segmentSize, int indent = 0) + { + using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); + writer.Indent = indent; + for (int i = 0; i < value.Length; i += segmentSize) + { + writer.Write(value, i, Math.Min(value.Length - i, segmentSize)); } - private static string WriteChars(char[] value, int maxLength, int indent = 0) - { - using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); - writer.Indent = indent; - foreach (var ch in value) - { - writer.Write(ch); - } + writer.Flush(); + return writer.BaseWriter.ToString()!; + } - writer.Flush(); - return writer.BaseWriter.ToString(); + private static string WriteChars(char[] value, int maxLength, int indent = 0) + { + using var writer = LineWrappingTextWriter.ForStringWriter(maxLength); + writer.Indent = indent; + foreach (var ch in value) + { + writer.Write(ch); } + + writer.Flush(); + return writer.BaseWriter.ToString()!; } } diff --git a/src/Ookii.CommandLine.Tests/NameTransformTest.cs b/src/Ookii.CommandLine.Tests/NameTransformTest.cs index b953c83f..a9baacc2 100644 --- a/src/Ookii.CommandLine.Tests/NameTransformTest.cs +++ b/src/Ookii.CommandLine.Tests/NameTransformTest.cs @@ -1,58 +1,57 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class NameTransformTest { - [TestClass] - public class NameTransformTest + [TestMethod] + public void TestNone() { - [TestMethod] - public void TestNone() - { - var transform = NameTransform.None; - Assert.AreEqual("TestName", transform.Apply("TestName")); - Assert.AreEqual("testName", transform.Apply("testName")); - Assert.AreEqual("__test__name__", transform.Apply("__test__name__")); - Assert.AreEqual("TestName", transform.Apply("TestName")); - } + var transform = NameTransform.None; + Assert.AreEqual("TestName", transform.Apply("TestName")); + Assert.AreEqual("testName", transform.Apply("testName")); + Assert.AreEqual("__test__name__", transform.Apply("__test__name__")); + Assert.AreEqual("TestName", transform.Apply("TestName")); + } - [TestMethod] - public void TestPascalCase() - { - var transform = NameTransform.PascalCase; - Assert.AreEqual("TestName", transform.Apply("TestName")); - Assert.AreEqual("TestName", transform.Apply("testName")); - Assert.AreEqual("TestName", transform.Apply("__test__name__")); - Assert.AreEqual("TestName", transform.Apply("TestName")); - } + [TestMethod] + public void TestPascalCase() + { + var transform = NameTransform.PascalCase; + Assert.AreEqual("TestName", transform.Apply("TestName")); + Assert.AreEqual("TestName", transform.Apply("testName")); + Assert.AreEqual("TestName", transform.Apply("__test__name__")); + Assert.AreEqual("TestName", transform.Apply("TestName")); + } - [TestMethod] - public void TestCamelCase() - { - var transform = NameTransform.CamelCase; - Assert.AreEqual("testName", transform.Apply("TestName")); - Assert.AreEqual("testName", transform.Apply("testName")); - Assert.AreEqual("testName", transform.Apply("__test__name__")); - Assert.AreEqual("testName", transform.Apply("TestName")); - } + [TestMethod] + public void TestCamelCase() + { + var transform = NameTransform.CamelCase; + Assert.AreEqual("testName", transform.Apply("TestName")); + Assert.AreEqual("testName", transform.Apply("testName")); + Assert.AreEqual("testName", transform.Apply("__test__name__")); + Assert.AreEqual("testName", transform.Apply("TestName")); + } - [TestMethod] - public void TestSnakeCase() - { - var transform = NameTransform.SnakeCase; - Assert.AreEqual("test_name", transform.Apply("TestName")); - Assert.AreEqual("test_name", transform.Apply("testName")); - Assert.AreEqual("test_name", transform.Apply("__test__name__")); - Assert.AreEqual("test_name", transform.Apply("TestName")); - } + [TestMethod] + public void TestSnakeCase() + { + var transform = NameTransform.SnakeCase; + Assert.AreEqual("test_name", transform.Apply("TestName")); + Assert.AreEqual("test_name", transform.Apply("testName")); + Assert.AreEqual("test_name", transform.Apply("__test__name__")); + Assert.AreEqual("test_name", transform.Apply("TestName")); + } - [TestMethod] - public void TestDashCase() - { - var transform = NameTransform.DashCase; - Assert.AreEqual("test-name", transform.Apply("TestName")); - Assert.AreEqual("test-name", transform.Apply("testName")); - Assert.AreEqual("test-name", transform.Apply("__test__name__")); - Assert.AreEqual("test-name", transform.Apply("TestName")); - } + [TestMethod] + public void TestDashCase() + { + var transform = NameTransform.DashCase; + Assert.AreEqual("test-name", transform.Apply("TestName")); + Assert.AreEqual("test-name", transform.Apply("testName")); + Assert.AreEqual("test-name", transform.Apply("__test__name__")); + Assert.AreEqual("test-name", transform.Apply("TestName")); } } diff --git a/src/Ookii.CommandLine.Tests/NetStandardHelpers.cs b/src/Ookii.CommandLine.Tests/NetStandardHelpers.cs index 25f39938..202a0143 100644 --- a/src/Ookii.CommandLine.Tests/NetStandardHelpers.cs +++ b/src/Ookii.CommandLine.Tests/NetStandardHelpers.cs @@ -4,52 +4,51 @@ using System.Collections.Generic; using System.Text; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +internal static class KeyValuePair { - internal static class KeyValuePair + public static KeyValuePair Create(TKey key, TValue value) { - public static KeyValuePair Create(TKey key, TValue value) - { - return new KeyValuePair(key, value); - } + return new KeyValuePair(key, value); } +} - internal static class StringExtensions - { - private static readonly char[] _newLineChars = { '\r', '\n' }; +internal static class StringExtensions +{ + private static readonly char[] _newLineChars = { '\r', '\n' }; - public static string ReplaceLineEndings(this string value, string ending = null) + public static string ReplaceLineEndings(this string value, string? ending = null) + { + ending ??= Environment.NewLine; + var result = new StringBuilder(); + int pos = 0; + while (pos < value.Length) { - ending ??= Environment.NewLine; - var result = new StringBuilder(); - int pos = 0; - while (pos < value.Length) + int index = value.IndexOfAny(_newLineChars, pos); + if (index < 0) { - int index = value.IndexOfAny(_newLineChars, pos); - if (index < 0) - { - result.Append(value.Substring(pos)); - break; - } - - if (index > pos) - { - result.Append(value.Substring(pos, index - pos)); - } - - result.Append(ending); - if (value[index] == '\r' && index + 1 < value.Length && value[index + 1] == '\n') - { - pos = index + 2; - } - else - { - pos = index + 1; - } + result.Append(value.Substring(pos)); + break; } - return result.ToString(); + if (index > pos) + { + result.Append(value.Substring(pos, index - pos)); + } + + result.Append(ending); + if (value[index] == '\r' && index + 1 < value.Length && value[index + 1] == '\n') + { + pos = index + 2; + } + else + { + pos = index + 1; + } } + + return result.ToString(); } } diff --git a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs new file mode 100644 index 00000000..5101c2ad --- /dev/null +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -0,0 +1,179 @@ +#if NET6_0_OR_GREATER +#nullable enable + +using Ookii.CommandLine.Conversion; +using System.Collections.Generic; +using System.Globalization; + +namespace Ookii.CommandLine.Tests; + +// We deliberately have some properties and methods that cause warnings, so disable those. +#pragma warning disable OCL0021,OCL0033 + +class NullReturningStringConverter : ArgumentConverter +{ + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + { + if (value == "(null)") + { + return null; + } + else + { + return value; + } + } +} + +class NullReturningIntConverter : ArgumentConverter +{ + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + { + if (value == "(null)") + { + return null; + } + else + { + return int.Parse(value); + } + } +} + +[GeneratedParser] +partial class NullableArguments +{ + [CommandLineArgument("constructorNullable", Position = 0)] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public string? ConstructorNullable { get; set; } + + [CommandLineArgument("constructorNonNullable", Position = 1)] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public string ConstructorNonNullable { get; set; } = default!; + + [CommandLineArgument("constructorValueType", Position = 2)] + [ArgumentConverter(typeof(NullReturningIntConverter))] + public int ConstructorValueType { get; set; } + + [CommandLineArgument("constructorNullableValueType", Position = 3)] + [ArgumentConverter(typeof(NullReturningIntConverter))] + public int? ConstructorNullableValueType { get; set; } + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public string? Nullable { get; set; } = "NotNullDefaultValue"; + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public string NonNullable { get; set; } = string.Empty; + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningIntConverter))] + public int ValueType { get; set; } + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningIntConverter))] + public int? NullableValueType { get; set; } = 42; + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public string[]? NonNullableArray { get; set; } + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningIntConverter))] + public int[]? ValueArray { get; set; } + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public ICollection NonNullableCollection { get; } = new List(); + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningIntConverter))] + [MultiValueSeparator(";")] + public ICollection ValueCollection { get; } = new List(); + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public string?[]? NullableArray { get; set; } + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningIntConverter))] + public string?[]? NullableValueArray { get; set; } + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public ICollection NullableCollection { get; } = new List(); + + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public ICollection NullableValueCollection { get; } = new List(); + + [CommandLineArgument] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] + public Dictionary? NonNullableDictionary { get; set; } + + [CommandLineArgument] + [ValueConverter(typeof(NullReturningIntConverter))] + public Dictionary? ValueDictionary { get; set; } + + [CommandLineArgument] + [ValueConverter(typeof(NullReturningStringConverter))] + public IDictionary NonNullableIDictionary { get; } = new Dictionary(); + + [CommandLineArgument] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningIntConverter))] + [MultiValueSeparator(";")] + public IDictionary ValueIDictionary { get; } = new Dictionary(); + + [CommandLineArgument] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] + public Dictionary? NullableDictionary { get; set; } + + [CommandLineArgument] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningIntConverter))] + public Dictionary? NullableValueDictionary { get; set; } + + [CommandLineArgument] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] + public IDictionary NullableIDictionary { get; } = new Dictionary(); + + [CommandLineArgument] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningIntConverter))] + [MultiValueSeparator(";")] + public IDictionary NullableValueIDictionary { get; } = new Dictionary(); + + // This is an incorrect type converter (doesn't return KeyValuePair), but it doesn't + // matter since it'll only be used to test null values. + [CommandLineArgument] + [ArgumentConverter(typeof(NullReturningStringConverter))] + public Dictionary? InvalidDictionary { get; set; } +} + +#endif + +#if NET7_0_OR_GREATER + +[GeneratedParser] +partial class RequiredPropertyArguments +{ + [CommandLineArgument] + public required string Arg1 { get; set; } + + [CommandLineArgument] + public string? Arg2 { get; set; } + + // IsRequired is ignored + [CommandLineArgument(IsRequired = false)] + public required string? Foo { get; init; } + + [CommandLineArgument] + public required int[] Bar { get; set; } +} + +#endif diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 79248260..70b44202 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -1,25 +1,31 @@ - + - net6.0;net48 - disable + net7.0;net6.0;net48 + enable + Ookii.CommandLine Unit Tests Tests for Ookii.CommandLine. false - 9.0 + 11.0 + true + true + ookii.snk + false - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + - diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs new file mode 100644 index 00000000..021ae087 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs @@ -0,0 +1,61 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class ParseOptionsAttributeTest +{ + [TestMethod] + public void TestConstructor() + { + var options = new ParseOptionsAttribute(); + Assert.IsTrue(options.AllowWhiteSpaceValueSeparator); + Assert.IsNull(options.ArgumentNamePrefixes); + Assert.AreEqual(NameTransform.None, options.ArgumentNameTransform); + Assert.IsTrue(options.AutoHelpArgument); + Assert.IsTrue(options.AutoPrefixAliases); + Assert.IsTrue(options.AutoVersionArgument); + Assert.IsFalse(options.CaseSensitive); + Assert.AreEqual(ErrorMode.Error, options.DuplicateArguments); + Assert.IsFalse(options.IsPosix); + Assert.IsNull(options.LongArgumentNamePrefix); + Assert.AreEqual(ParsingMode.Default, options.Mode); + Assert.IsNull(options.NameValueSeparators); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); + } + + [TestMethod] + public void TestIsPosix() + { + var options = new ParseOptionsAttribute() + { + IsPosix = true + }; + + Assert.IsTrue(options.IsPosix); + Assert.AreEqual(ParsingMode.LongShort, options.Mode); + Assert.IsTrue(options.CaseSensitive); + Assert.AreEqual(NameTransform.DashCase, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.DashCase, options.ValueDescriptionTransform); + options.CaseSensitive = false; + Assert.IsFalse(options.IsPosix); + options.CaseSensitive = true; + Assert.IsTrue(options.IsPosix); + + options.IsPosix = false; + Assert.AreEqual(ParsingMode.Default, options.Mode); + Assert.IsFalse(options.CaseSensitive); + Assert.AreEqual(NameTransform.None, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); + + options = new ParseOptionsAttribute() + { + Mode = ParsingMode.LongShort, + CaseSensitive = true, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase + }; + + Assert.IsTrue(options.IsPosix); + } +} diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs new file mode 100644 index 00000000..613768e7 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -0,0 +1,143 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Terminal; +using System; +using System.Globalization; +using System.Linq; + +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class ParseOptionsTest +{ + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // In case other tests had changed this. + ParseOptions.ForceReflectionDefault = false; + } + + [TestMethod] + public void TestConstructor() + { + var options = new ParseOptions(); + Assert.IsNull(options.AllowWhiteSpaceValueSeparator); + Assert.IsNull(options.ArgumentNameComparison); + Assert.IsNull(options.ArgumentNamePrefixes); + Assert.IsNull(options.ArgumentNameTransform); + Assert.IsNull(options.AutoHelpArgument); + Assert.IsNull(options.AutoPrefixAliases); + Assert.IsNull(options.AutoVersionArgument); + Assert.AreEqual(CultureInfo.InvariantCulture, options.Culture); + Assert.IsNull(options.DefaultValueDescriptions); + Assert.IsNull(options.DuplicateArguments); + Assert.IsNull(options.Error); + Assert.AreEqual(TextFormat.ForegroundRed, options.ErrorColor); + Assert.IsFalse(options.ForceReflection); + Assert.IsFalse(options.IsPosix); + Assert.IsNull(options.LongArgumentNamePrefix); + Assert.IsNull(options.Mode); + Assert.IsNull(options.NameValueSeparators); + Assert.AreEqual(UsageHelpRequest.SyntaxOnly, options.ShowUsageOnError); + Assert.IsNotNull(options.StringProvider); + Assert.IsNotNull(options.UsageWriter); + Assert.IsNull(options.UseErrorColor); + Assert.IsNull(options.ValueDescriptionTransform); + Assert.AreEqual(TextFormat.ForegroundYellow, options.WarningColor); + + // Defaults + Assert.IsTrue(options.AllowWhiteSpaceValueSeparatorOrDefault); + CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), options.ArgumentNamePrefixesOrDefault.ToArray()); + Assert.AreEqual(NameTransform.None, options.ArgumentNameTransformOrDefault); + Assert.IsTrue(options.AutoHelpArgumentOrDefault); + Assert.IsTrue(options.AutoPrefixAliasesOrDefault); + Assert.IsTrue(options.AutoVersionArgumentOrDefault); + Assert.AreEqual(StringComparison.OrdinalIgnoreCase, options.ArgumentNameComparisonOrDefault); + Assert.AreEqual(ErrorMode.Error, options.DuplicateArgumentsOrDefault); + Assert.AreEqual("--", options.LongArgumentNamePrefixOrDefault); + Assert.AreEqual(ParsingMode.Default, options.ModeOrDefault); + CollectionAssert.AreEqual(new[] { ':', '=' }, options.NameValueSeparatorsOrDefault.ToArray()); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransformOrDefault); + } + + [TestMethod] + public void TestIsPosix() + { + var options = new ParseOptions() + { + IsPosix = true + }; + + Assert.IsTrue(options.IsPosix); + Assert.AreEqual(ParsingMode.LongShort, options.Mode); + Assert.AreEqual(StringComparison.InvariantCulture, options.ArgumentNameComparison); + Assert.AreEqual(NameTransform.DashCase, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.DashCase, options.ValueDescriptionTransform); + options.ArgumentNameComparison = StringComparison.CurrentCultureIgnoreCase; + Assert.IsFalse(options.IsPosix); + options.ArgumentNameComparison = StringComparison.CurrentCulture; + Assert.IsTrue(options.IsPosix); + + options.IsPosix = false; + Assert.AreEqual(ParsingMode.Default, options.Mode); + Assert.AreEqual(StringComparison.OrdinalIgnoreCase, options.ArgumentNameComparison); + Assert.AreEqual(NameTransform.None, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); + + options = new ParseOptions() + { + Mode = ParsingMode.LongShort, + ArgumentNameComparison = StringComparison.InvariantCulture, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase + }; + + Assert.IsTrue(options.IsPosix); + } + + [TestMethod] + public void TestMerge() + { + var options = new ParseOptions(); + var attribute = new ParseOptionsAttribute(); + options.Merge(attribute); + Assert.IsTrue(options.AllowWhiteSpaceValueSeparator); + Assert.IsNull(options.ArgumentNamePrefixes); + Assert.AreEqual(NameTransform.None, options.ArgumentNameTransform); + Assert.IsTrue(options.AutoHelpArgument); + Assert.IsTrue(options.AutoPrefixAliases); + Assert.IsTrue(options.AutoVersionArgument); + Assert.AreEqual(StringComparison.OrdinalIgnoreCase, options.ArgumentNameComparison); + Assert.AreEqual(ErrorMode.Error, options.DuplicateArguments); + Assert.IsFalse(options.IsPosix); + Assert.IsNull(options.LongArgumentNamePrefix); + Assert.AreEqual(ParsingMode.Default, options.Mode); + Assert.IsNull(options.NameValueSeparators); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); + + options = new ParseOptions(); + attribute = new ParseOptionsAttribute() + { + CaseSensitive = true, + ArgumentNamePrefixes = new[] { "+", "++" }, + LongArgumentNamePrefix = "+++", + }; + + options.Merge(attribute); + Assert.AreEqual(StringComparison.InvariantCulture, options.ArgumentNameComparison); + CollectionAssert.AreEqual(new[] { "+", "++" }, options.ArgumentNamePrefixes!.ToArray()); + Assert.AreEqual("+++", options.LongArgumentNamePrefix); + + options = new ParseOptions(); + attribute = new ParseOptionsAttribute() + { + IsPosix = true, + }; + + options.Merge(attribute); + Assert.IsTrue(options.IsPosix); + Assert.AreEqual(ParsingMode.LongShort, options.Mode); + Assert.AreEqual(StringComparison.InvariantCulture, options.ArgumentNameComparison); + Assert.AreEqual(NameTransform.DashCase, options.ArgumentNameTransform); + Assert.AreEqual(NameTransform.DashCase, options.ValueDescriptionTransform); + } +} diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs new file mode 100644 index 00000000..f527172c --- /dev/null +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs @@ -0,0 +1,155 @@ +namespace Ookii.CommandLine.Tests; + +partial class SubCommandTest +{ + private const string _executableName = "test"; + + public static readonly string _expectedUsage = @"Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + custom + Custom parsing command. + + test + Test command description. + + version + Displays version information. + +".ReplaceLineEndings(); + + public static readonly string _expectedUsageNoVersion = @"Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + custom + Custom parsing command. + + test + Test command description. + +".ReplaceLineEndings(); + + public static readonly string _expectedUsageColor = @"Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + custom + Custom parsing command. + + test + Test command description. + + version + Displays version information. + +".ReplaceLineEndings(); + + public static readonly string _expectedUsageInstruction = @"Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + custom + Custom parsing command. + + test + Test command description. + + version + Displays version information. + +Run 'test -Help' for more information about a command. +".ReplaceLineEndings(); + + public static readonly string _expectedUsageAutoInstruction = @"Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + test + Test command description. + + version + Displays version information. + +Run 'test -Help' for more information about a command. +".ReplaceLineEndings(); + + + public static readonly string _expectedUsageWithDescription = @"Tests for Ookii.CommandLine. + +Usage: test [arguments] + +The following commands are available: + + AnotherSimpleCommand, alias + + custom + Custom parsing command. + + test + Test command description. + + version + Displays version information. + +".ReplaceLineEndings(); + + public static readonly string _expectedCommandUsage = @"Async command description. + +Usage: test AsyncCommand [[-Value] ] [-Help] + + -Value + Argument description. + + -Help [] (-?, -h) + Displays this help message. + +".ReplaceLineEndings(); + + public static readonly string _expectedParentCommandUsage = @"Parent command description. + +Usage: test TestParentCommand [arguments] + +The following commands are available: + + NestedParentCommand + Other parent command description. + + OtherTestChildCommand + + TestChildCommand + +Run 'test TestParentCommand -Help' for more information about a command. +".ReplaceLineEndings(); + + public static readonly string _expectedNestedParentCommandUsage = @"Other parent command description. + +Usage: test TestParentCommand NestedParentCommand [arguments] + +The following commands are available: + + NestedParentChildCommand + +Run 'test TestParentCommand NestedParentCommand -Help' for more information about a command. +".ReplaceLineEndings(); + + public static readonly string _expectedNestedChildCommandUsage = @"Unknown argument name 'Foo'. + +Usage: test TestParentCommand NestedParentCommand NestedParentChildCommand [-Help] + + -Help [] (-?, -h) + Displays this help message. + +".ReplaceLineEndings(); +} diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index d11004a7..200df9fb 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -1,427 +1,601 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Support; +using Ookii.CommandLine.Tests.Commands; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +[TestClass] +public partial class SubCommandTest { - [TestClass] - public class SubCommandTest + private static readonly Assembly _commandAssembly = Assembly.GetExecutingAssembly(); + + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) { - private static readonly Assembly _commandAssembly = Assembly.GetExecutingAssembly(); + // Get test coverage of reflection provider even on types that have the + // GeneratedParserAttribute. + ParseOptions.ForceReflectionDefault = true; + } - [TestMethod] - public void GetCommandsTest() - { - var manager = new CommandManager(_commandAssembly); - var commands = manager.GetCommands().ToArray(); - - Assert.IsNotNull(commands); - Assert.AreEqual(6, commands.Length); - - int index = 0; - VerifyCommand(commands[index++], "AnotherSimpleCommand", typeof(AnotherSimpleCommand), false, new[] { "alias" }); - VerifyCommand(commands[index++], "AsyncCommand", typeof(AsyncCommand)); - VerifyCommand(commands[index++], "custom", typeof(CustomParsingCommand), true); - VerifyCommand(commands[index++], "HiddenCommand", typeof(HiddenCommand)); - VerifyCommand(commands[index++], "test", typeof(TestCommand)); - VerifyCommand(commands[index++], "version", null); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void GetCommandsTest(ProviderKind kind) + { + var manager = CreateManager(kind); + VerifyCommands( + manager.GetCommands(), + new("AnotherSimpleCommand", typeof(AnotherSimpleCommand), false, "alias"), + new("AsyncCommand", typeof(AsyncCommand)), + new("custom", typeof(CustomParsingCommand), true), + new("HiddenCommand", typeof(HiddenCommand)), + new("test", typeof(TestCommand)), + new("TestParentCommand", typeof(TestParentCommand), true), + new("version", null) + ); + } - [TestMethod] - public void GetCommandTest() - { - var manager = new CommandManager(_commandAssembly); - var command = manager.GetCommand("test"); - Assert.IsNotNull(command); - Assert.AreEqual("test", command.Value.Name); - Assert.AreEqual(typeof(TestCommand), command.Value.CommandType); - - command = manager.GetCommand("wrong"); - Assert.IsNull(command); - - command = manager.GetCommand("Test"); // default is case-insensitive - Assert.IsNotNull(command); - Assert.AreEqual("test", command.Value.Name); - Assert.AreEqual(typeof(TestCommand), command.Value.CommandType); - - var manager2 = new CommandManager(_commandAssembly, new CommandOptions() { CommandNameComparer = StringComparer.Ordinal }); - command = manager2.GetCommand("Test"); - Assert.IsNull(command); - - command = manager.GetCommand("AnotherSimpleCommand"); - Assert.IsNotNull(command); - Assert.AreEqual("AnotherSimpleCommand", command.Value.Name); - Assert.AreEqual(typeof(AnotherSimpleCommand), command.Value.CommandType); - - command = manager.GetCommand("alias"); - Assert.IsNotNull(command); - Assert.AreEqual("AnotherSimpleCommand", command.Value.Name); - Assert.AreEqual(typeof(AnotherSimpleCommand), command.Value.CommandType); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void GetCommandTest(ProviderKind kind) + { + var manager = CreateManager(kind); + var command = manager.GetCommand("test"); + Assert.IsNotNull(command); + Assert.AreEqual("test", command.Name); + Assert.AreEqual(typeof(TestCommand), command.CommandType); + + command = manager.GetCommand("wrong"); + Assert.IsNull(command); + + command = manager.GetCommand("Test"); // default is case-insensitive + Assert.IsNotNull(command); + Assert.AreEqual("test", command.Name); + Assert.AreEqual(typeof(TestCommand), command.CommandType); + + var manager2 = new CommandManager(_commandAssembly, new CommandOptions() { CommandNameComparison = StringComparison.Ordinal, AutoCommandPrefixAliases = false }); + command = manager2.GetCommand("Test"); + Assert.IsNull(command); + + command = manager.GetCommand("AnotherSimpleCommand"); + Assert.IsNotNull(command); + Assert.AreEqual("AnotherSimpleCommand", command.Name); + Assert.AreEqual(typeof(AnotherSimpleCommand), command.CommandType); + + command = manager.GetCommand("alias"); + Assert.IsNotNull(command); + Assert.AreEqual("AnotherSimpleCommand", command.Name); + Assert.AreEqual(typeof(AnotherSimpleCommand), command.CommandType); + + // Can't get a command with an parent that's not currently set in the options. + command = manager.GetCommand("TestChildCommand"); + Assert.IsNull(command); + } - [TestMethod] - public void IsCommandTest() - { - bool isCommand = CommandInfo.IsCommand(typeof(TestCommand)); - Assert.IsTrue(isCommand); + [TestMethod] + public void IsCommandTest() + { + bool isCommand = CommandInfo.IsCommand(typeof(TestCommand)); + Assert.IsTrue(isCommand); - isCommand = CommandInfo.IsCommand(typeof(NotACommand)); - Assert.IsFalse(isCommand); - } + isCommand = CommandInfo.IsCommand(typeof(NotACommand)); + Assert.IsFalse(isCommand); + } - [TestMethod] - public void CreateCommandTest() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void CreateCommandTest(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() + Error = writer, + UsageWriter = new UsageWriter(writer) { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - } - }; - - var manager = new CommandManager(_commandAssembly, options); - TestCommand command = (TestCommand)manager.CreateCommand("test", new[] { "-Argument", "Foo" }, 0); - Assert.IsNotNull(command); - Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); - Assert.AreEqual("Foo", command.Argument); - Assert.AreEqual("", writer.BaseWriter.ToString()); - - command = (TestCommand)manager.CreateCommand(new[] { "test", "-Argument", "Bar" }); - Assert.IsNotNull(command); - Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); - Assert.AreEqual("Bar", command.Argument); - Assert.AreEqual("", writer.BaseWriter.ToString()); - - var command2 = (AnotherSimpleCommand)manager.CreateCommand("anothersimplecommand", new[] { "skip", "-Value", "42" }, 1); - Assert.IsNotNull(command2); - Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); - Assert.AreEqual(42, command2.Value); - Assert.AreEqual("", writer.BaseWriter.ToString()); - - CustomParsingCommand command3 = (CustomParsingCommand)manager.CreateCommand(new[] { "custom", "hello" }); - Assert.IsNotNull(command3); - // None because of custom parsing. - Assert.AreEqual(ParseStatus.None, manager.ParseResult.Status); - Assert.AreEqual("hello", command3.Value); - Assert.AreEqual("", writer.BaseWriter.ToString()); - - var versionCommand = manager.CreateCommand(new[] { "version" }); - Assert.IsNotNull(versionCommand); - Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); - Assert.AreEqual("", writer.BaseWriter.ToString()); - - options.AutoVersionCommand = false; - versionCommand = manager.CreateCommand(new[] { "version" }); - Assert.IsNull(versionCommand); - Assert.AreEqual(ParseStatus.None, manager.ParseResult.Status); - Assert.AreEqual(_expectedUsageNoVersion, writer.BaseWriter.ToString()); - - ((StringWriter)writer.BaseWriter).GetStringBuilder().Clear(); - versionCommand = manager.CreateCommand(new[] { "test", "-Foo" }); - Assert.IsNull(versionCommand); - Assert.AreEqual(ParseStatus.Error, manager.ParseResult.Status); - Assert.AreEqual(CommandLineArgumentErrorCategory.UnknownArgument, manager.ParseResult.LastException.Category); - Assert.AreEqual(manager.ParseResult.ArgumentName, manager.ParseResult.LastException.ArgumentName); - Assert.AreNotEqual("", writer.BaseWriter.ToString()); - - } + ExecutableName = _executableName, + } + }; + + var manager = CreateManager(kind, options); + var command = (TestCommand?)manager.CreateCommand("test", new[] { "-Argument", "Foo" }); + Assert.IsNotNull(command); + Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); + Assert.AreEqual("Foo", command.Argument); + Assert.AreEqual("", writer.BaseWriter.ToString()); + + command = (TestCommand?)manager.CreateCommand(new[] { "test", "-Argument", "Bar" }); + Assert.IsNotNull(command); + Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); + Assert.AreEqual("Bar", command.Argument); + Assert.AreEqual("", writer.BaseWriter.ToString()); + + var command2 = (AnotherSimpleCommand?)manager.CreateCommand("anothersimplecommand", (new[] { "skip", "-Value", "42" }).AsMemory(1)); + Assert.IsNotNull(command2); + Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); + Assert.AreEqual(42, command2.Value); + Assert.AreEqual("", writer.BaseWriter.ToString()); + + var command3 = (CustomParsingCommand?)manager.CreateCommand(new[] { "custom", "hello" }); + Assert.IsNotNull(command3); + // None because of custom parsing. + Assert.AreEqual(ParseStatus.None, manager.ParseResult.Status); + Assert.AreEqual("hello", command3.Value); + Assert.AreEqual("", writer.BaseWriter.ToString()); + + var versionCommand = manager.CreateCommand(new[] { "version" }); + Assert.IsNotNull(versionCommand); + Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); + Assert.AreEqual("", writer.BaseWriter.ToString()); + + options.AutoVersionCommand = false; + versionCommand = manager.CreateCommand(new[] { "version" }); + Assert.IsNull(versionCommand); + Assert.AreEqual(ParseStatus.None, manager.ParseResult.Status); + Assert.AreEqual(_expectedUsageNoVersion, writer.BaseWriter.ToString()); + + ((StringWriter)writer.BaseWriter).GetStringBuilder().Clear(); + versionCommand = manager.CreateCommand(new[] { "test", "-Foo" }); + Assert.IsNull(versionCommand); + Assert.AreEqual(ParseStatus.Error, manager.ParseResult.Status); + Assert.AreEqual(CommandLineArgumentErrorCategory.UnknownArgument, manager.ParseResult.LastException!.Category); + Assert.AreEqual(manager.ParseResult.ArgumentName, manager.ParseResult.LastException.ArgumentName); + Assert.AreNotEqual("", writer.BaseWriter.ToString()); - [TestMethod] - public void TestWriteUsage() - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - } - }; - - var manager = new CommandManager(_commandAssembly, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsage, writer.BaseWriter.ToString()); - } + } - [TestMethod] - public void TestWriteUsageColor() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsage(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() + Error = writer, + UsageWriter = new UsageWriter(writer) { - Error = writer, - UsageWriter = new UsageWriter(writer, true) - { - ExecutableName = _executableName, - } - }; - - var manager = new CommandManager(_commandAssembly, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsageColor, writer.BaseWriter.ToString()); - } + ExecutableName = _executableName, + } + }; - [TestMethod] - public void TestWriteUsageInstruction() - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - IncludeCommandHelpInstruction = true, - } - }; - - var manager = new CommandManager(_commandAssembly, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsageInstruction, writer.BaseWriter.ToString()); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsage, writer.BaseWriter.ToString()); + } - [TestMethod] - public void TestWriteUsageApplicationDescription() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageColor(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() + Error = writer, + UsageWriter = new UsageWriter(writer, true) { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - IncludeApplicationDescriptionBeforeCommandList = true, - ExecutableName = _executableName, - } - }; - - var manager = new CommandManager(_commandAssembly, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsageWithDescription, writer.BaseWriter.ToString()); - } + ExecutableName = _executableName, + } + }; - [TestMethod] - public void TestCommandUsage() - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - } - }; - - // This tests whether the command name is included in the help for the command. - var manager = new CommandManager(_commandAssembly, options); - var result = manager.CreateCommand(new[] { "AsyncCommand", "-Help" }); - Assert.IsNull(result); - Assert.AreEqual(ParseStatus.Canceled, manager.ParseResult.Status); - Assert.AreEqual("Help", manager.ParseResult.ArgumentName); - Assert.AreEqual(_expectedCommandUsage, writer.BaseWriter.ToString()); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageColor, writer.BaseWriter.ToString()); + } - [TestMethod] - public void TestCommandNameTransform() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageInstruction(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - var options = new CommandOptions() + Error = writer, + UsageWriter = new UsageWriter(writer) { - CommandNameTransform = NameTransform.PascalCase - }; - - var manager = new CommandManager(_commandAssembly, options); - var info = new CommandInfo(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("AnotherSimple", info.Name); - - options.CommandNameTransform = NameTransform.CamelCase; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("anotherSimple", info.Name); - - options.CommandNameTransform = NameTransform.SnakeCase; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("another_simple", info.Name); - - options.CommandNameTransform = NameTransform.DashCase; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("another-simple", info.Name); - - options.StripCommandNameSuffix = null; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("another-simple-command", info.Name); - - options.StripCommandNameSuffix = "Command"; - Assert.IsNotNull(manager.GetCommand("another-simple")); + ExecutableName = _executableName, + IncludeCommandHelpInstruction = true, + } + }; - // Check automatic command name is affected too. - options.CommandNameTransform = NameTransform.PascalCase; - Assert.AreEqual("Version", manager.GetCommand("Version")?.Name); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageInstruction, writer.BaseWriter.ToString()); + } - [TestMethod] - public void TestCommandFilter() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageAutoInstruction(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - var options = new CommandOptions() + Error = writer, + // Filter out commands that prevent the instruction being shown. + CommandFilter = c => !c.UseCustomArgumentParsing, + UsageWriter = new UsageWriter(writer) { - CommandFilter = cmd => !cmd.UseCustomArgumentParsing, - }; - - var manager = new CommandManager(_commandAssembly, options); - Assert.IsNull(manager.GetCommand("custom")); - Assert.IsNotNull(manager.GetCommand("test")); - Assert.IsNotNull(manager.GetCommand("AnotherSimpleCommand")); - Assert.IsNotNull(manager.GetCommand("HiddenCommand")); - } - - [TestMethod] - public async Task TestAsyncCommand() - { - var manager = new CommandManager(_commandAssembly); - var result = await manager.RunCommandAsync(new[] { "AsyncCommand", "5" }); - Assert.AreEqual(5, result); - - // RunCommand works but calls Run. - result = manager.RunCommand(new[] { "AsyncCommand", "5" }); - Assert.AreEqual(6, result); + ExecutableName = _executableName, + IncludeCommandHelpInstruction = true, + } + }; - // RunCommandAsync works on non-async tasks. - result = await manager.RunCommandAsync(new[] { "AnotherSimpleCommand", "-Value", "5" }); - Assert.AreEqual(5, result); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageAutoInstruction, writer.BaseWriter.ToString()); + } - [TestMethod] - public async Task TestAsyncCommandBase() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageApplicationDescription(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - var command = new AsyncBaseCommand(); - var actual = await command.RunAsync(); - Assert.AreEqual(42, actual); + Error = writer, + UsageWriter = new UsageWriter(writer) + { + IncludeApplicationDescriptionBeforeCommandList = true, + ExecutableName = _executableName, + } + }; - // Test Run invokes RunAsync. - actual = command.Run(); - Assert.AreEqual(42, actual); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageWithDescription, writer.BaseWriter.ToString()); + } - private static void VerifyCommand(CommandInfo command, string name, Type type, bool customParsing = false, string[] aliases = null) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandUsage(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - Assert.AreEqual(name, command.Name); - if (type != null) + Error = writer, + UsageWriter = new UsageWriter(writer) { - Assert.AreEqual(type, command.CommandType); + ExecutableName = _executableName, } + }; + + // This tests whether the command name is included in the help for the command. + var manager = CreateManager(kind, options); + var result = manager.CreateCommand(new[] { "AsyncCommand", "-Help" }); + Assert.IsNull(result); + Assert.AreEqual(ParseStatus.Canceled, manager.ParseResult.Status); + Assert.AreEqual("Help", manager.ParseResult.ArgumentName); + Assert.AreEqual(_expectedCommandUsage, writer.BaseWriter.ToString()); + } - Assert.AreEqual(customParsing, command.UseCustomArgumentParsing); - CollectionAssert.AreEqual(aliases ?? Array.Empty(), command.Aliases.ToArray()); - } - - #region Expected usage - - private const string _executableName = "test"; + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandNameTransform(ProviderKind kind) + { + var options = new CommandOptions() + { + CommandNameTransform = NameTransform.PascalCase + }; - public static readonly string _expectedUsage = @"Usage: test [arguments] + var manager = CreateManager(kind, options); + var info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("AnotherSimple", info.Name); -The following commands are available: + options.CommandNameTransform = NameTransform.CamelCase; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("anotherSimple", info.Name); - AnotherSimpleCommand, alias + options.CommandNameTransform = NameTransform.SnakeCase; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("another_simple", info.Name); - custom - Custom parsing command. + options.CommandNameTransform = NameTransform.DashCase; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("another-simple", info.Name); - test - Test command description. + options.StripCommandNameSuffix = null; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("another-simple-command", info.Name); - version - Displays version information. + options.StripCommandNameSuffix = "Command"; + Assert.IsNotNull(manager.GetCommand("another-simple")); -".ReplaceLineEndings(); + // Check automatic command name is affected too. + options.CommandNameTransform = NameTransform.PascalCase; + Assert.AreEqual("Version", manager.GetCommand("Version")?.Name); + } - public static readonly string _expectedUsageNoVersion = @"Usage: test [arguments] + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandFilter(ProviderKind kind) + { + var options = new CommandOptions() + { + CommandFilter = cmd => !cmd.UseCustomArgumentParsing, + }; + + var manager = CreateManager(kind, options); + Assert.IsNull(manager.GetCommand("custom")); + Assert.IsNotNull(manager.GetCommand("test")); + Assert.IsNotNull(manager.GetCommand("AnotherSimpleCommand")); + Assert.IsNotNull(manager.GetCommand("HiddenCommand")); + } -The following commands are available: + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public async Task TestAsyncCommand(ProviderKind kind) + { + var manager = CreateManager(kind); + var result = await manager.RunCommandAsync(new[] { "AsyncCommand", "5" }); + Assert.AreEqual(5, result); - AnotherSimpleCommand, alias + // RunCommand works but calls Run. + result = manager.RunCommand(new[] { "AsyncCommand", "5" }); + Assert.AreEqual(6, result); - custom - Custom parsing command. + // RunCommandAsync works on non-async tasks. + result = await manager.RunCommandAsync(new[] { "AnotherSimpleCommand", "-Value", "5" }); + Assert.AreEqual(5, result); + } - test - Test command description. + [TestMethod] + public async Task TestAsyncCommandBase() + { + var command = new AsyncBaseCommand(); + var actual = await command.RunAsync(); + Assert.AreEqual(42, actual); -".ReplaceLineEndings(); + // Test Run invokes RunAsync. + actual = command.Run(); + Assert.AreEqual(42, actual); + } - public static readonly string _expectedUsageColor = @"Usage: test [arguments] + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestExplicitAssembly(ProviderKind kind) + { + if (kind == ProviderKind.Reflection) + { + // Using the calling assembly explicitly loads all the commands, including internal, + // same as the default constructor. + var mgr = new CommandManager(_commandAssembly); + Assert.AreEqual(7, mgr.GetCommands().Count()); + } -The following commands are available: + // Explicitly specify the external assembly, which loads only public commands. + var manager = kind == ProviderKind.Reflection + ? new CommandManager(typeof(ExternalCommand).Assembly) + : new GeneratedManagerWithExplicitAssembly(); + + VerifyCommands( + manager.GetCommands(), + new("external", typeof(ExternalCommand)), + new("OtherExternalCommand", typeof(OtherExternalCommand)), + new("version", null) + ); + + // Public commands from external assembly plus public and internal commands from + // calling assembly. + manager = kind == ProviderKind.Reflection + ? new CommandManager(new[] { typeof(ExternalCommand).Assembly, _commandAssembly }) + : new GeneratedManagerWithMultipleAssemblies(); + + VerifyCommands( + manager.GetCommands(), + new("AnotherSimpleCommand", typeof(AnotherSimpleCommand), false, "alias"), + new("AsyncCommand", typeof(AsyncCommand)), + new("custom", typeof(CustomParsingCommand), true), + new("external", typeof(ExternalCommand)), + new("HiddenCommand", typeof(HiddenCommand)), + new("OtherExternalCommand", typeof(OtherExternalCommand)), + new("test", typeof(TestCommand)), + new("TestParentCommand", typeof(TestParentCommand), true), + new("version", null) + ); + } - AnotherSimpleCommand, alias + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestParentCommand(ProviderKind kind) + { + var options = new CommandOptions + { + ParentCommand = typeof(TestParentCommand), + AutoCommandPrefixAliases = false, + }; + + var manager = CreateManager(kind, options); + VerifyCommands( + manager.GetCommands(), + new("NestedParentCommand", typeof(NestedParentCommand), true) { ParentCommand = typeof(TestParentCommand) }, + new("OtherTestChildCommand", typeof(OtherTestChildCommand)) { ParentCommand = typeof(TestParentCommand) }, + new("TestChildCommand", typeof(TestChildCommand)) { ParentCommand = typeof(TestParentCommand) } + ); + + var command = manager.GetCommand("TestChildCommand"); + Assert.IsNotNull(command); + + command = manager.GetCommand("version"); + Assert.IsNull(command); + + command = manager.GetCommand("test"); + Assert.IsNull(command); + + manager.Options.ParentCommand = null; + var result = manager.RunCommand(new[] { "TestParentCommand", "TestChildCommand", "-Value", "5" }); + Assert.AreEqual(5, result); + } - custom - Custom parsing command. + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestParentCommandUsage(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() + { + Error = writer, + ShowUsageOnError = UsageHelpRequest.Full, + UsageWriter = new UsageWriter(writer) + { + ExecutableName = _executableName, + IncludeCommandHelpInstruction = true, + IncludeApplicationDescriptionBeforeCommandList = true, + } + }; + + var manager = CreateManager(kind, options); + var result = manager.RunCommand(new[] { "TestParentCommand" }); + Assert.AreEqual(1, result); + Assert.AreEqual(_expectedParentCommandUsage, writer.ToString()); + + ((StringWriter)writer.BaseWriter).GetStringBuilder().Clear(); + result = manager.RunCommand(new[] { "TestParentCommand", "NestedParentCommand" }); + Assert.AreEqual(1, result); + Assert.AreEqual(_expectedNestedParentCommandUsage, writer.ToString()); + + ((StringWriter)writer.BaseWriter).GetStringBuilder().Clear(); + result = manager.RunCommand(new[] { "TestParentCommand", "NestedParentCommand", "NestedParentChildCommand", "-Foo" }); + Assert.AreEqual(1, result); + Assert.AreEqual(_expectedNestedChildCommandUsage, writer.ToString()); + } - test - Test command description. + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAutoPrefixAliases(ProviderKind kind) + { + var manager = CreateManager(kind); - version - Displays version information. + // Ambiguous between test and TestParentCommand. + Assert.IsNull(manager.GetCommand("tes")); -".ReplaceLineEndings(); + // Not ambiguous + Assert.AreEqual("TestParentCommand", manager.GetCommand("testp")!.Name); + Assert.AreEqual("version", manager.GetCommand("v")!.Name); - public static readonly string _expectedUsageInstruction = @"Usage: test [arguments] + // Case sensitive, "tes" is no longer ambigous. + manager = CreateManager(kind, new CommandOptions() { CommandNameComparison = StringComparison.Ordinal }); + Assert.AreEqual("test", manager.GetCommand("tes")!.Name); + } -The following commands are available: + private class VersionCommandStringProvider : LocalizedStringProvider + { + public override string AutomaticVersionCommandName() => "AnotherSimpleCommand"; + } - AnotherSimpleCommand, alias + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestVersionCommandConflict(ProviderKind kind) + { + // Change the name of the version command so it matches one of the explicit commands. + var options = new CommandOptions() + { + StringProvider = new VersionCommandStringProvider(), + }; - custom - Custom parsing command. + var manager = CreateManager(kind, options); - test - Test command description. + // There is no command named version. + Assert.IsNull(manager.GetCommand("version")); - version - Displays version information. + // Name returns our command. + Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("AnotherSimpleCommand")!.CommandType); -Run 'test -Help' for more information about a command. -".ReplaceLineEndings(); + // There is only one in the list of commands. + Assert.AreEqual(1, manager.GetCommands().Where(c => c.Name == "AnotherSimpleCommand").Count()); - public static readonly string _expectedUsageWithDescription = @"Tests for Ookii.CommandLine. + // Prefix is not ambiguous because the automatic command doesn't exist. + Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("Another")!.CommandType); -Usage: test [arguments] + // If we filter out our command, the automatic one gets returned. + options.CommandFilter = c => c.CommandType != typeof(AnotherSimpleCommand); + Assert.AreEqual(typeof(AutomaticVersionCommand), manager.GetCommand("AnotherSimpleCommand")!.CommandType); + Assert.AreEqual(typeof(AutomaticVersionCommand), manager.GetCommands().Where(c => c.Name == "AnotherSimpleCommand").Single().CommandType); + } -The following commands are available: + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNoVersionCommand(ProviderKind kind) + { + var options = new CommandOptions() + { + AutoVersionCommand = false, + }; + + var manager = CreateManager(kind, options); + Assert.IsNull(manager.GetCommand("version")); + Assert.IsFalse(manager.GetCommands().Any(c => c.Name == "version")); + + // We can also filter it out. + options.AutoVersionCommand = true; + Assert.IsNotNull(manager.GetCommand("version")); + options.CommandFilter = c => c.Name != "version"; + Assert.IsNull(manager.GetCommand("version")); + Assert.IsFalse(manager.GetCommands().Any(c => c.Name == "version")); + + // Setting ParentCommand means there is no version command. + options.CommandFilter = null; + options.ParentCommand = typeof(ParentCommand); + Assert.IsNull(manager.GetCommand("version")); + Assert.IsFalse(manager.GetCommands().Any(c => c.Name == "version")); + } - AnotherSimpleCommand, alias + private record struct ExpectedCommand(string Name, Type? Type, bool CustomParsing = false, params string[]? Aliases) + { + public Type ParentCommand { get; set; } + } - custom - Custom parsing command. - test - Test command description. + private static void VerifyCommand(CommandInfo command, string name, Type? type, bool customParsing = false, string[]? aliases = null) + { + Assert.AreEqual(name, command.Name); + if (type != null) + { + Assert.AreEqual(type, command.CommandType); + } - version - Displays version information. + Assert.AreEqual(customParsing, command.UseCustomArgumentParsing); + CollectionAssert.AreEqual(aliases ?? Array.Empty(), command.Aliases.ToArray()); + } -".ReplaceLineEndings(); + private static void VerifyCommands(IEnumerable actual, params ExpectedCommand[] expected) + { + Assert.AreEqual(expected.Length, actual.Count()); + var index = 0; + foreach (var command in actual) + { + var info = expected[index]; + VerifyCommand(command, info.Name, info.Type, info.CustomParsing, info.Aliases); + Assert.AreEqual(info.ParentCommand, command.ParentCommandType); + ++index; + } + } - public static readonly string _expectedCommandUsage = @"Async command description. -Usage: test AsyncCommand [[-Value] ] [-Help] + public static CommandManager CreateManager(ProviderKind kind, CommandOptions? options = null) + { + var manager = kind switch + { + ProviderKind.Reflection => new CommandManager(options), + ProviderKind.Generated => new GeneratedManager(options), + _ => throw new InvalidOperationException() + }; - -Value - Argument description. + Assert.AreEqual(kind, manager.ProviderKind); + return manager; + } - -Help [] (-?, -h) - Displays this help message. + public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) + => $"{methodInfo.Name} ({data[0]})"; -".ReplaceLineEndings(); - #endregion - } + public static IEnumerable ProviderKinds + => new[] + { + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } + }; } diff --git a/src/Ookii.CommandLine.Tests/TextFormatTest.cs b/src/Ookii.CommandLine.Tests/TextFormatTest.cs new file mode 100644 index 00000000..ce159ca6 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/TextFormatTest.cs @@ -0,0 +1,44 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Terminal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Tests; + +[TestClass] +public class TextFormatTest +{ + [TestMethod] + public void TestDefault() + { + var value = new TextFormat(); + Assert.AreEqual("", value.Value); + + var value2 = default(TextFormat); + Assert.AreEqual("", value2.Value); + } + + [TestMethod] + public void TestAddition() + { + var value = TextFormat.ForegroundRed + TextFormat.BackgroundGreen; + Assert.AreEqual("\x1b[31m\x1b[42m", value.Value); + } + + [TestMethod] + public void TestEquality() + { + Assert.AreEqual(TextFormat.ForegroundRed, TextFormat.ForegroundRed); + Assert.AreNotEqual(TextFormat.ForegroundGreen, TextFormat.ForegroundRed); + var value1 = TextFormat.ForegroundRed; + var value2 = TextFormat.ForegroundRed; + Assert.IsTrue(value1 == value2); + Assert.IsFalse(value1 != value2); + value2 = TextFormat.ForegroundGreen; + Assert.IsFalse(value1 == value2); + Assert.IsTrue(value1 != value2); + } +} diff --git a/src/Ookii.CommandLine.Tests/ookii.snk b/src/Ookii.CommandLine.Tests/ookii.snk new file mode 100644 index 00000000..1befa134 Binary files /dev/null and b/src/Ookii.CommandLine.Tests/ookii.snk differ diff --git a/src/Ookii.CommandLine.sln b/src/Ookii.CommandLine.sln index 469d689b..ff17ca06 100644 --- a/src/Ookii.CommandLine.sln +++ b/src/Ookii.CommandLine.sln @@ -32,6 +32,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArgumentDependencies", "Sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wpf", "Samples\Wpf\Wpf.csproj", "{1D7E3B10-D99E-4DF9-9AB7-5DDFF61B275D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ookii.CommandLine.Generator", "Ookii.CommandLine.Generator\Ookii.CommandLine.Generator.csproj", "{9C027C37-4BEA-422F-A148-1F73C6FFEF45}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ookii.CommandLine.Tests.Commands", "Ookii.CommandLine.Tests.Commands\Ookii.CommandLine.Tests.Commands.csproj", "{05AEDC31-D784-4DCA-A431-4A55323DEAFA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TopLevelArguments", "Samples\TopLevelArguments\TopLevelArguments.csproj", "{1FF02963-CECD-4C90-8C44-68F1B1CF5988}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,6 +80,18 @@ Global {1D7E3B10-D99E-4DF9-9AB7-5DDFF61B275D}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D7E3B10-D99E-4DF9-9AB7-5DDFF61B275D}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D7E3B10-D99E-4DF9-9AB7-5DDFF61B275D}.Release|Any CPU.Build.0 = Release|Any CPU + {9C027C37-4BEA-422F-A148-1F73C6FFEF45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C027C37-4BEA-422F-A148-1F73C6FFEF45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C027C37-4BEA-422F-A148-1F73C6FFEF45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C027C37-4BEA-422F-A148-1F73C6FFEF45}.Release|Any CPU.Build.0 = Release|Any CPU + {05AEDC31-D784-4DCA-A431-4A55323DEAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05AEDC31-D784-4DCA-A431-4A55323DEAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05AEDC31-D784-4DCA-A431-4A55323DEAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05AEDC31-D784-4DCA-A431-4A55323DEAFA}.Release|Any CPU.Build.0 = Release|Any CPU + {1FF02963-CECD-4C90-8C44-68F1B1CF5988}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FF02963-CECD-4C90-8C44-68F1B1CF5988}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FF02963-CECD-4C90-8C44-68F1B1CF5988}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FF02963-CECD-4C90-8C44-68F1B1CF5988}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,6 +104,7 @@ Global {5E22EACC-46A7-4906-BFBB-ED2F9B77DB65} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} {8717BF8D-9D9A-4DC6-8C03-B17F51D708CC} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} {1D7E3B10-D99E-4DF9-9AB7-5DDFF61B275D} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} + {1FF02963-CECD-4C90-8C44-68F1B1CF5988} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6E22AD53-E031-474F-8AC7-B247C4311820} diff --git a/src/Ookii.CommandLine/AliasAttribute.cs b/src/Ookii.CommandLine/AliasAttribute.cs index 69c2d10e..59d7beb4 100644 --- a/src/Ookii.CommandLine/AliasAttribute.cs +++ b/src/Ookii.CommandLine/AliasAttribute.cs @@ -1,67 +1,68 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Defines an alternative name for a command line argument or subcommand. +/// +/// +/// +/// To specify multiple aliases, apply this attribute multiple times. +/// +/// +/// The aliases for a command line argument can be used instead of their regular name to specify +/// the parameter on the command line. For example, this can be used to have a shorter name for an +/// argument (e.g. "-v" as an alternative to "-Verbose"). +/// +/// +/// All regular command line argument names and aliases used by an instance of the +/// class must be unique. +/// +/// +/// By default, the command line usage help generated by the +/// method includes the aliases. Set the +/// property to to exclude them. +/// +/// +/// If the property is +/// , and the argument this is applied to does +/// not have a long name, this attribute is ignored. +/// +/// +/// This attribute can also be applied to classes that implement the +/// interface to specify an alias for that command. In that case, inclusion of the aliases in +/// the command list usage help is controlled by the +/// property. +/// +/// +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = true)] +public sealed class AliasAttribute : Attribute { + private readonly string _alias; + /// - /// Defines an alternative name for a command line argument or a subcommand. + /// Initializes a new instance of the class. /// - /// - /// - /// To specify multiple aliases, apply this attribute multiple times. - /// - /// - /// The aliases for a command line argument can be used instead of their regular name to specify the parameter on the command line. - /// For example, this can be used to have a shorter name for an argument (e.g. "-v" as an alternative to "-Verbose"). - /// - /// - /// All regular command line argument names and aliases used by an instance of the class must be - /// unique. - /// - /// - /// By default, the command line usage help generated by the - /// method includes the aliases. Set the - /// property to to exclude them. - /// - /// - /// If the property is , and the argument - /// this is applied to does not have a long name, this attribute is ignored. - /// - /// - /// This attribute can also be applied to classes that implement the - /// interface to specify an alias for that command. In that case, inclusion of the aliases in - /// the command list usage help is controlled by the - /// property. - /// - /// - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = true)] - public sealed class AliasAttribute : Attribute + /// The alternative name for the command line argument or subcommand. + /// + /// is . + /// + public AliasAttribute(string alias) { - private readonly string _alias; - - /// - /// Initializes a new instance of the class. - /// - /// The alternative name for the command line argument or subcommand. - /// - /// is . - /// - public AliasAttribute(string alias) - { - _alias = alias ?? throw new ArgumentNullException(nameof(alias)); - } + _alias = alias ?? throw new ArgumentNullException(nameof(alias)); + } - /// - /// Gets the alternative name for the command line argument or subcommand. - /// - /// - /// The alternative name. - /// - public string Alias - { - get { return _alias; } - } + /// + /// Gets the alternative name for the command line argument or subcommand. + /// + /// + /// The alternative name. + /// + public string Alias + { + get { return _alias; } } } diff --git a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs index 6d04ab71..a6c8b895 100644 --- a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs +++ b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs @@ -1,29 +1,31 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; using System.Collections.Generic; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates that a dictionary argument accepts the same key more than once. +/// +/// +/// +/// If this attribute is applied to an argument whose type is +/// or another type that implements the interface, a +/// duplicate key will simply overwrite the previous value. +/// +/// +/// If this attribute is not applied, a with a +/// the property set to +/// will +/// be thrown when a duplicate key is specified. +/// +/// +/// The is ignored if it is applied to any +/// other type of argument. +/// +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public sealed class AllowDuplicateDictionaryKeysAttribute : Attribute { - /// - /// Indicates that a dictionary argument accepts the same key more than once. - /// - /// - /// - /// If this attribute is applied to an argument whose type is or - /// , a duplicate key will simply overwrite the previous value. - /// - /// - /// If this attribute is not applied, a with a - /// of will be thrown when a duplicate key is specified. - /// - /// - /// The is ignored if it is applied to any other type of argument. - /// - /// - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public sealed class AllowDuplicateDictionaryKeysAttribute : Attribute - { - } } diff --git a/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs b/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs index fd975a92..af60d029 100644 --- a/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs +++ b/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs @@ -1,50 +1,52 @@ using System; +using System.Reflection; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Sets the friendly name of the application to be used in the output of the "-Version" +/// argument or "version" subcommand. +/// +/// +/// +/// This attribute is used when a "-Version" argument is automatically added to the arguments +/// of your application, and by the automatically added "version" subcommand. It can be applied to +/// the type defining command line arguments, or to the assembly that contains it. +/// +/// +/// If not present, the automatic "-Version" argument and "version" command will use the +/// value of the attribute, or the assembly name of the +/// assembly containing the arguments type. +/// +/// +/// For the "version" subcommand, this attribute must be applied to the entry point assembly of +/// your application. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)] +public class ApplicationFriendlyNameAttribute : Attribute { + private readonly string _name; + /// - /// Sets the friendly name of the application to be used in the output of the "-Version" - /// argument or "version" subcommand. + /// Initializes a new instance of the + /// attribute. /// - /// - /// - /// This attribute is used when a "-Version" argument is automatically added to the arguments - /// of your application. It can be applied to the type defining command line arguments, or - /// to the assembly that contains it. - /// - /// - /// If not present, the automatic "-Version" argument will use the assembly name of the - /// assembly containing the arguments type. - /// - /// - /// It is also used by the automatically created "version" command. - /// - /// - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)] - public class ApplicationFriendlyNameAttribute : Attribute + /// The friendly name of the application. + /// + /// is . + /// + public ApplicationFriendlyNameAttribute(string name) { - private readonly string _name; - - /// - /// Initializes a new instance of the - /// attribute. - /// - /// The friendly name of the application. - /// - /// is . - /// - public ApplicationFriendlyNameAttribute(string name) - { - _name = name ?? throw new ArgumentNullException(nameof(name)); - } - - /// - /// Gets the friendly name of the application. - /// - /// - /// The friendly name of the application. - /// - public string Name => _name; + _name = name ?? throw new ArgumentNullException(nameof(name)); } + + /// + /// Gets the friendly name of the application. + /// + /// + /// The friendly name of the application. + /// + public string Name => _name; } diff --git a/src/Ookii.CommandLine/ArgumentKind.cs b/src/Ookii.CommandLine/ArgumentKind.cs index 4d7c2db2..e56ae9e2 100644 --- a/src/Ookii.CommandLine/ArgumentKind.cs +++ b/src/Ookii.CommandLine/ArgumentKind.cs @@ -1,27 +1,26 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Specifies what kind of argument an instance of the class +/// represents. +/// +public enum ArgumentKind { /// - /// Specifies what kind of argument an instance of the class - /// represents. + /// A regular argument that can have only a single value. /// - public enum ArgumentKind - { - /// - /// A regular argument that can have only a single value. - /// - SingleValue, - /// - /// A multi-value argument. - /// - MultiValue, - /// - /// A dictionary argument, which is a multi-value argument where the values are key/value - /// pairs with unique keys. - /// - Dictionary, - /// - /// An argument that invokes a method when specified. - /// - Method - } + SingleValue, + /// + /// A multi-value argument. + /// + MultiValue, + /// + /// A dictionary argument, which is a multi-value argument where the values are key/value + /// pairs with unique keys. + /// + Dictionary, + /// + /// An argument that invokes a method when specified. + /// + Method } diff --git a/src/Ookii.CommandLine/ArgumentNameAttribute.cs b/src/Ookii.CommandLine/ArgumentNameAttribute.cs deleted file mode 100644 index f595dd80..00000000 --- a/src/Ookii.CommandLine/ArgumentNameAttribute.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; - -namespace Ookii.CommandLine -{ - /// - /// Indicates an alternative argument name for an argument defined by a constructor parameter. - /// - /// - /// - /// Apply the attribute to a constructor parameter to indicate - /// that the name of the argument should be different than the parameter name, or to specify - /// a short name if the property is . - /// - /// - /// If no argument name is specified, the parameter name will be used, applying the - /// specified by the - /// property or the property. - /// - /// - /// The will not be applied to names specified with this - /// attribute. - /// - /// - /// For arguments defined using properties or methods, use the - /// attribute. - /// - /// - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class ArgumentNameAttribute : Attribute - { - private readonly string? _argumentName; - private bool _short; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The name of the argument, or to indicate the parameter name - /// should be used (applying the that is being used). - /// - /// - /// - /// The will not be applied to explicitly specified names. - /// - /// - /// If the property is , - /// is the long name of the attribute. - /// - /// - public ArgumentNameAttribute(string? argumentName = null) - { - _argumentName = argumentName; - } - - /// - /// Gets the name of the argument. - /// - /// - /// The name of the argument. - /// - /// - /// - /// If the property is , - /// this is the long name of the attribute. - /// - /// - /// If the property is , - /// and the property is , this property will - /// not be used. - /// - /// - /// - public string? ArgumentName => _argumentName; - - /// - /// Gets or sets a value that indicates whether the argument has a long name. - /// - /// - /// if the argument has a long name; otherwise, . - /// The default value is . - /// - /// - /// - /// This property is ignored if is not - /// . - /// - /// - /// If the property is , - /// and this property is , the property - /// will not be used. - /// - /// - /// - public bool IsLong { get; set; } = true; - - /// - /// Gets or sets a value that indicates whether the argument has a short name. - /// - /// - /// if the argument has a short name; otherwise, . - /// The default value is . - /// - /// - /// - /// This property is ignored if is not - /// . - /// - /// - /// If the property is not set but this property is set to - /// , the short name will be derived using the first character of - /// the long name. - /// - /// - /// /// - public bool IsShort - { - get => _short || ShortName != '\0'; - set => _short = value; - } - /// - /// Gets or sets the argument's short name. - /// - /// The short name, or a null character ('\0') if the argument has no short name. - /// - /// - /// This property is ignored if is not - /// . - /// - /// - /// If this property is not set but the property is set to , - /// the short name will be derived using the first character of the long name. - /// - /// - /// /// - public char ShortName { get; set; } - } -} diff --git a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs index 83b7a456..3f0eda56 100644 --- a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs +++ b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs @@ -1,64 +1,58 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; -using System.ComponentModel; +using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides data for the event. +/// +/// +public class ArgumentParsedEventArgs : EventArgs { + private readonly CommandLineArgument _argument; + + /// + /// Initializes a new instance of the class. + /// + /// The argument that has been parsed. + /// is . + public ArgumentParsedEventArgs(CommandLineArgument argument) + { + _argument = argument ?? throw new ArgumentNullException(nameof(argument)); + } + /// - /// Provides data for the event. + /// Gets the argument that was parsed. /// + /// + /// The instance for the argument. + /// + public CommandLineArgument Argument + { + get { return _argument; } + } + + /// + /// Gets 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 the + /// value of the + /// property, or the return value of a method argument. + /// /// /// - /// If the event handler sets the property to , command line processing will stop immediately, - /// and the method will return , even if all the required positional parameters have already - /// been parsed. + /// 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 class ArgumentParsedEventArgs : CancelEventArgs - { - private readonly CommandLineArgument _argument; - - /// - /// Initializes a new instance of the class. - /// - /// The argument that has been parsed. - /// is . - public ArgumentParsedEventArgs(CommandLineArgument argument) - { - _argument = argument ?? throw new ArgumentNullException(nameof(argument)); - } - - /// - /// Gets the argument that was parsed. - /// - /// - /// The instance for the argument. - /// - public CommandLineArgument Argument - { - get { return _argument; } - } - - /// - /// Gets or sets a value that indicates whether or not the - /// property should be ignored. - /// - /// - /// if argument parsing should continue even if the argument has - /// set to ; - /// otherwise, . The default value is . - /// - /// - /// - /// This property does not affect the property. - /// If is set to , parsing - /// is always canceled regardless of the value of . - /// - /// - public bool OverrideCancelParsing { get; set; } - } + /// + /// + /// + public CancelMode CancelParsing { get; set; } } diff --git a/src/Ookii.CommandLine/BreakLineMode.cs b/src/Ookii.CommandLine/BreakLineMode.cs index 6f824ca1..e878c295 100644 --- a/src/Ookii.CommandLine/BreakLineMode.cs +++ b/src/Ookii.CommandLine/BreakLineMode.cs @@ -1,9 +1,8 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +internal enum BreakLineMode { - internal enum BreakLineMode - { - Backward, - Forward, - Force - } + Backward, + Forward, + Force } \ No newline at end of file diff --git a/src/Ookii.CommandLine/CancelMode.cs b/src/Ookii.CommandLine/CancelMode.cs new file mode 100644 index 00000000..f659c973 --- /dev/null +++ b/src/Ookii.CommandLine/CancelMode.cs @@ -0,0 +1,27 @@ +namespace Ookii.CommandLine; + +/// +/// Indicates whether and how an argument should cancel parsing. +/// +/// +/// +public enum CancelMode +{ + /// + /// The argument does not cancel parsing. + /// + None, + /// + /// The argument cancels parsing, discarding the results so far. Parsing, using for example the + /// method, will return a + /// value. The property will be . + /// + Abort, + /// + /// The argument cancels parsing, returning success using the results so far. Remaining + /// arguments are not parsed, and will be available in the + /// property. The property will be . + /// If not all required arguments have values at this point, an exception will be thrown. + /// + Success +} diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index dc6af731..fe1df5fa 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1,1920 +1,1590 @@ -// Copyright (c) Sven Groot (Ookii.org) +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides information about command line arguments that are recognized by an instance of the +/// class. +/// +/// +/// +public abstract class CommandLineArgument { - /// - /// Provides information about command line arguments that are recognized by a . - /// - /// - /// - public sealed class CommandLineArgument + #region Nested types + + private interface IValueHelper { - #region Nested types + object? Value { get; } + CancelMode SetValue(CommandLineArgument argument, object? value); + void ApplyValue(CommandLineArgument argument, object target); + } - private interface IValueHelper + private class SingleValueHelper : IValueHelper + { + public SingleValueHelper(object? initialValue) { - object? Value { get; } - bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value); - void ApplyValue(object target, PropertyInfo property); + Value = initialValue; } - private class SingleValueHelper : IValueHelper - { - public SingleValueHelper(object? initialValue) - { - Value = initialValue; - } - - public object? Value { get; private set; } - - public void ApplyValue(object target, PropertyInfo property) - { - property.SetValue(target, Value); - } + public object? Value { get; private set; } - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) - { - Value = value; - return true; - } + public void ApplyValue(CommandLineArgument argument, object target) + { + argument.SetProperty(target, Value); } - private class MultiValueHelper : IValueHelper + public CancelMode SetValue(CommandLineArgument argument, object? value) { - // The actual element type may not be nullable. This is handled by the allow null check - // when parsing the value. Here, we always treat the values as if they're nullable. - private readonly List _values = new(); - - public object? Value => _values.ToArray(); - - public void ApplyValue(object target, PropertyInfo property) - { - if (property.PropertyType.IsArray) - { - property.SetValue(target, Value); - return; - } + Value = value; + return CancelMode.None; + } + } - object? collection = property.GetValue(target, null); - if (collection == null) - { - throw new InvalidOperationException(); - } + private class MultiValueHelper : IValueHelper + { + // The actual element type may not be nullable. This is handled by the allow null check + // when parsing the value. Here, we always treat the values as if they're nullable. + private readonly List _values = new(); - var list = (ICollection)collection; - list.Clear(); - foreach (var value in _values) - { - list.Add(value); - } - } + public object? Value => _values.ToArray(); - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + public void ApplyValue(CommandLineArgument argument, object target) + { + if (argument.CanSetProperty) { - _values.Add((T?)value); - return true; + argument.SetProperty(target, Value); + return; } - } - private class DictionaryValueHelper : IValueHelper - where TKey : notnull - { - // The actual value type may not be nullable. This is handled by the allow null check. - private readonly Dictionary _dictionary = new(); - private readonly bool _allowDuplicateKeys; - private readonly bool _allowNullValues; + var list = (ICollection?)argument.GetProperty(target) + ?? throw new InvalidOperationException(Properties.Resources.NullPropertyValue); - public DictionaryValueHelper(bool allowDuplicateKeys, bool allowNullValues) + list.Clear(); + foreach (var value in _values) { - _allowDuplicateKeys = allowDuplicateKeys; - _allowNullValues = allowNullValues; + list.Add(value); } + } - public object? Value => _dictionary; + public CancelMode SetValue(CommandLineArgument argument, object? value) + { + _values.Add((T?)value); + return CancelMode.None; + } + } - public void ApplyValue(object target, PropertyInfo property) - { - if (property.GetSetMethod() != null) - { - property.SetValue(target, _dictionary); - return; - } + private class DictionaryValueHelper : IValueHelper + where TKey : notnull + { + // The actual value type may not be nullable. This is handled by the allow null check. + private readonly Dictionary _dictionary = new(); + private readonly bool _allowDuplicateKeys; + private readonly bool _allowNullValues; - var dictionary = (IDictionary?)property.GetValue(target, null); - if (dictionary == null) - { - throw new InvalidOperationException(); - } + public DictionaryValueHelper(bool allowDuplicateKeys, bool allowNullValues) + { + _allowDuplicateKeys = allowDuplicateKeys; + _allowNullValues = allowNullValues; + } - dictionary.Clear(); - foreach (var pair in _dictionary) - { - dictionary.Add(pair.Key, pair.Value); - } - } + public object? Value => _dictionary; - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + public void ApplyValue(CommandLineArgument argument, object target) + { + if (argument.CanSetProperty) { - // ConvertToArgumentType is guaranteed to return non-null for dictionary arguments. - var pair = (KeyValuePair)value!; + argument.SetProperty(target, _dictionary); + return; + } - // With the KeyValuePairConverter, these should already be checked, but it's still - // checked here to deal with custom converters. - if (pair.Key == null || (!_allowNullValues && pair.Value == null)) - { - throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, argument); - } + var dictionary = (IDictionary?)argument.GetProperty(target) + ?? throw new InvalidOperationException(Properties.Resources.NullPropertyValue); - try - { - if (_allowDuplicateKeys) - { - _dictionary[pair.Key] = pair.Value; - } - else - { - _dictionary.Add(pair.Key, pair.Value); - } - } - catch (ArgumentException ex) - { - throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.InvalidDictionaryValue, ex, argument, value.ToString()); - } - - return true; + dictionary.Clear(); + foreach (var pair in _dictionary) + { + dictionary.Add(pair.Key, pair.Value); } } - private class MethodValueHelper : IValueHelper + public CancelMode SetValue(CommandLineArgument argument, object? value) { - public object? Value { get; private set; } + // ConvertToArgumentType is guaranteed to return non-null for dictionary arguments. + var pair = (KeyValuePair)value!; - public void ApplyValue(object target, PropertyInfo property) + // With the KeyValuePairConverter, these should already be checked, but it's still + // checked here to deal with custom converters. + if (pair.Key == null || (!_allowNullValues && pair.Value == null)) { - throw new InvalidOperationException(); + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, argument); } - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + try { - Value = value; - var info = argument._method!.Value; - int parameterCount = (info.HasValueParameter ? 1 : 0) + (info.HasParserParameter ? 1 : 0); - var parameters = new object?[parameterCount]; - int index = 0; - if (info.HasValueParameter) - { - parameters[index] = Value; - ++index; - } - - if (info.HasParserParameter) - { - parameters[index] = argument._parser; - } - - var returnValue = info.Method.Invoke(null, parameters); - if (returnValue == null) + if (_allowDuplicateKeys) { - return true; + _dictionary[pair.Key] = pair.Value; } else { - return (bool)returnValue; + _dictionary.Add(pair.Key, pair.Value); } } - } + catch (ArgumentException ex) + { + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.InvalidDictionaryValue, ex, argument, value.ToString()); + } - private struct ArgumentInfo - { - public CommandLineParser Parser { get; set; } - public PropertyInfo? Property { get; set; } - public MethodArgumentInfo? Method { get; set; } - public ParameterInfo? Parameter { get; set; } - public string MemberName { get; set; } - public string ArgumentName { get; set; } - public bool Long { get; set; } - public bool Short { get; set; } - public char ShortName { get; set; } - public IEnumerable? Aliases { get; set; } - public IEnumerable? ShortAliases { get; set; } - public Type ArgumentType { get; set; } - public Type? ConverterType { get; set; } - public Type? KeyConverterType { get; set; } - public Type? ValueConverterType { get; set; } - public int? Position { get; set; } - public bool IsRequired { get; set; } - public object? DefaultValue { get; set; } - public string? Description { get; set; } - public string? ValueDescription { get; set; } - public string? MultiValueSeparator { get; set; } - public bool AllowMultiValueWhiteSpaceSeparator { get; set; } - public string? KeyValueSeparator { get; set; } - public bool AllowDuplicateDictionaryKeys { get; set; } - public bool AllowNull { get; set; } - public bool CancelParsing { get; set; } - public bool IsHidden { get; set; } - public IEnumerable Validators { get; set; } + return CancelMode.None; } + } + + private class MethodValueHelper : IValueHelper + { + public object? Value { get; private set; } - private struct MethodArgumentInfo + public void ApplyValue(CommandLineArgument argument, object target) { - public MethodInfo Method { get; set; } - public bool HasValueParameter { get; set; } - public bool HasParserParameter { get; set; } + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } - #endregion - - private readonly CommandLineParser _parser; - private readonly TypeConverter _converter; - private readonly PropertyInfo? _property; - private readonly MethodArgumentInfo? _method; - private readonly string _valueDescription; - private readonly string _argumentName; - private readonly bool _hasLongName = true; - private readonly char _shortName; - private readonly ReadOnlyCollection? _aliases; - private readonly ReadOnlyCollection? _shortAliases; - private readonly Type _argumentType; - private readonly Type _elementType; - 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 _allowDuplicateDictionaryKeys; - private readonly string? _multiValueSeparator; - private readonly bool _allowMultiValueWhiteSpaceSeparator; - private readonly string? _keyValueSeparator; - private readonly bool _allowNull; - private readonly bool _cancelParsing; - private readonly bool _isHidden; - private readonly IEnumerable _validators; - private IValueHelper? _valueHelper; - - private CommandLineArgument(ArgumentInfo info) - { - // If this method throws anything other than a NotSupportedException, it constitutes a bug in the Ookii.CommandLine library. - _parser = info.Parser; - _property = info.Property; - _method = info.Method; - _memberName = info.MemberName; - _argumentName = info.ArgumentName; - if (_parser.Mode == ParsingMode.LongShort) + public CancelMode SetValue(CommandLineArgument argument, object? value) + { + Value = value; + try { - _hasLongName = info.Long; - if (info.Short) - { - if (info.ShortName != '\0') - { - _shortName = info.ShortName; - } - else - { - _shortName = _argumentName[0]; - } - } - - if (!HasLongName) - { - if (!HasShortName) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoLongOrShortName, _argumentName)); - } - - _argumentName = _shortName.ToString(); - } + return argument.CallMethod(value); } - - if (HasLongName && info.Aliases != null) + catch (TargetInvocationException ex) { - _aliases = new(info.Aliases.ToArray()); + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, argument, value?.ToString()); } - - if (HasShortName && info.ShortAliases != null) + catch (Exception ex) { - _shortAliases = new(info.ShortAliases.ToArray()); + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex, argument, value?.ToString()); } + } + } - _argumentType = info.ArgumentType; - _elementTypeWithNullable = info.ArgumentType; - _description = info.Description; - _isRequired = info.IsRequired; - _allowNull = info.AllowNull; - _cancelParsing = info.CancelParsing; - _validators = info.Validators; - // Required or positional arguments cannot be hidden. - _isHidden = info.IsHidden && !info.IsRequired && info.Position == null; - Position = info.Position; - var converterType = info.ConverterType; - - if (_method == null) - { - var (collectionType, dictionaryType, elementType) = DetermineMultiValueType(); - - if (dictionaryType != null) - { - Debug.Assert(elementType != null); - _argumentKind = ArgumentKind.Dictionary; - _elementTypeWithNullable = elementType!; - _allowDuplicateDictionaryKeys = info.AllowDuplicateDictionaryKeys; - _allowNull = DetermineDictionaryValueTypeAllowsNull(dictionaryType, info.Property, info.Parameter); - _keyValueSeparator = info.KeyValueSeparator ?? KeyValuePairConverter.DefaultSeparator; - var genericArguments = dictionaryType.GetGenericArguments(); - if (converterType == null) - { - converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); - _converter = (TypeConverter)Activator.CreateInstance(converterType, _parser.StringProvider, _argumentName, _allowNull, info.KeyConverterType, info.ValueConverterType, _keyValueSeparator)!; - } + private class HelpArgument : CommandLineArgument + { + public HelpArgument(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + : base(CreateInfo(parser, argumentName, shortName, shortAlias)) + { + } - var valueDescription = info.ValueDescription ?? GetDefaultValueDescription(_elementTypeWithNullable, - _parser.Options.DefaultValueDescriptions); + protected override bool CanSetProperty => false; - if (valueDescription == null) - { - var key = DetermineValueDescription(genericArguments[0].GetUnderlyingType(), _parser.Options); - var value = DetermineValueDescription(genericArguments[1].GetUnderlyingType(), _parser.Options); - valueDescription = $"{key}{_keyValueSeparator}{value}"; - } + private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + { + var info = new ArgumentInfo() + { + Parser = parser, + ArgumentName = argumentName, + Kind = ArgumentKind.Method, + Long = true, + Short = true, + ShortName = parser.StringProvider.AutomaticHelpShortName(), + ArgumentType = typeof(bool), + ElementTypeWithNullable = typeof(bool), + ElementType = typeof(bool), + Description = parser.StringProvider.AutomaticHelpDescription(), + MemberName = "AutomaticHelp", + CancelParsing = CancelMode.Abort, + Validators = Enumerable.Empty(), + Converter = Conversion.BooleanConverter.Instance, + }; - _valueDescription = valueDescription; - } - else if (collectionType != null) + if (parser.Mode == ParsingMode.LongShort) + { + if (parser.ShortArgumentNameComparer!.Compare(shortAlias, shortName) != 0) { - Debug.Assert(elementType != null); - _argumentKind = ArgumentKind.MultiValue; - _elementTypeWithNullable = elementType!; - _allowNull = DetermineCollectionElementTypeAllowsNull(collectionType, info.Property, info.Parameter); + info.ShortAliases = new[] { shortAlias }; } } else { - _argumentKind = ArgumentKind.Method; - } - - // If it's a Nullable, now get the underlying type. - _elementType = _elementTypeWithNullable.GetUnderlyingType(); - - if (IsMultiValue) - { - _multiValueSeparator = info.MultiValueSeparator; - _allowMultiValueWhiteSpaceSeparator = !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; - } - - // Use the original Nullable for this if it is one. - if (_converter == null) - { - _converter = _elementTypeWithNullable.GetStringConverter(converterType); - } - - if (_valueDescription == null) - { - _valueDescription = info.ValueDescription ?? DetermineValueDescription(_elementType, _parser.Options); + var shortNameString = shortName.ToString(); + var shortAliasString = shortAlias.ToString(); + info.Aliases = string.Compare(shortAliasString, shortNameString, parser.ArgumentNameComparison) == 0 + ? new[] { shortNameString } + : new[] { shortNameString, shortAliasString }; } - _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); + return info; } - /// - /// Gets the that this argument belongs to. - /// - /// - /// An instance of the class. - /// - public CommandLineParser Parser => _parser; - - /// - /// Gets the name of the property, method, or constructor parameter that defined this command line argument. - /// - /// - /// The name of the property, method, or constructor parameter that defined this command line argument. - /// - public string MemberName - { - get { return _memberName; } - } + protected override CancelMode CallMethod(object? value) => CancelMode.Abort; - /// - /// Gets the name of this argument. - /// - /// - /// The name of this argument. - /// - /// - /// - /// This name is used to supply an argument value by name on the command line, and to describe the argument in the usage help - /// generated by . - /// - /// - /// If the property is , - /// and the property is , this returns - /// the short name of the argument. Otherwise, it returns the long name. - /// - /// - /// - /// - public string ArgumentName => _argumentName; - - /// - /// Gets the short name of this argument. - /// - /// - /// The short name of the argument, or a null character ('\0') if it doesn't have one. - /// - /// - /// - /// The short name is only used if the parser is using . - /// - /// - /// - /// - public char ShortName => _shortName; - - /// - /// Gets the name of this argument, with the appropriate argument name prefix. - /// - /// - /// The name of the argument, with an argument name prefix. - /// - /// - /// - /// If the property is , - /// this will use the long name with the long argument prefix if the argument has a long - /// name, and the short name with the primary short argument prefix if not. - /// - /// - /// For , the prefix used is the first prefix specified - /// in the property. - /// - /// - public string ArgumentNameWithPrefix - { - get - { - var prefix = (_parser.Mode == ParsingMode.LongShort && HasLongName) - ? _parser.LongArgumentNamePrefix - : _parser.ArgumentNamePrefixes[0]; - - return prefix + _argumentName; - } - } - - /// - /// Gets the long argument name with the long prefix. - /// - /// - /// The long argument name with its prefix, or if the - /// property is not or the - /// property is . - /// - public string? LongNameWithPrefix - { - get - { - return (_parser.Mode == ParsingMode.LongShort && HasLongName) - ? _parser.LongArgumentNamePrefix + _argumentName - : null; - } - } + protected override object? GetProperty(object target) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); - /// - /// Gets the short argument name with the primary short prefix. - /// - /// - /// The short argument name with its prefix, or if the - /// property is not or the - /// property is . - /// - /// - /// - /// The prefix used is the first prefix specified in the - /// property. - /// - /// - public string? ShortNameWithPrefix - { - get - { - return (_parser.Mode == ParsingMode.LongShort && HasShortName) - ? _parser.ArgumentNamePrefixes[0] + _shortName - : null; - } - } + protected override void SetProperty(object target, object? value) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + } - /// - /// Gets a value that indicates whether the argument has a short name. - /// - /// - /// if the argument has a short name; otherwise, . - /// - /// - /// - /// The short name is only used if the parser is using . - /// Otherwise, this property is always . - /// - /// - /// - /// - public bool HasShortName => _shortName != '\0'; - - /// - /// Gets a value that indicates whether the argument has a long name. - /// - /// - /// if the argument has a long name; otherwise, . - /// - /// - /// - /// If the property is not , - /// this property is always . - /// - /// - /// - /// - public bool HasLongName => _hasLongName; - - /// - /// Gets the alternative names for this command line argument. - /// - /// - /// A list of alternative names for this command line argument, or an empty collection if none were specified. - /// - /// - /// - /// If the property is , - /// and the property is , this property - /// will always return an empty collection. - /// - /// - /// - public ReadOnlyCollection? Aliases => _aliases; - - /// - /// Gets the alternative short names for this command line argument. - /// - /// - /// A list of alternative short names for this command line argument, or an empty collection if none were specified. - /// - /// - /// - /// If the property is not , - /// or the property is , this property - /// will always return an empty collection. - /// - /// - /// - public ReadOnlyCollection? ShortAliases => _shortAliases; - - /// - /// Gets the type of the argument's value. - /// - /// - /// The of the argument. - /// - public Type ArgumentType - { - get { return _argumentType; } + private class VersionArgument : CommandLineArgument + { + public VersionArgument(CommandLineParser parser, string argumentName) + : base(CreateInfo(parser, argumentName)) + { } - /// - /// Gets the type of the elements of the argument value. - /// - /// - /// If the property is , the - /// of each individual value; if the argument type is an instance of , - /// the type T; otherwise, the same value as the - /// property. - /// - public Type ElementType => _elementType; - - /// - /// Gets the position of this argument. - /// - /// - /// The position of this argument, or if this is not a positional argument. - /// - /// - /// - /// A positional argument is created either using a constructor parameter on the command line arguments type, - /// or by using the property. - /// - /// - /// The property reflects the actual position of the positional argument. For positional - /// arguments created from properties this doesn't need to match the original value of the property. - /// - /// - public int? Position { get; internal set; } - - /// - /// Gets a value that indicates whether the argument is required. - /// - /// - /// if the argument's value must be specified on the command line; if the argument may be omitted. - /// - /// - /// - /// An argument defined by a constructor parameter is required if the parameter does not - /// have a default value. An argument defined by a property or method is required if its - /// property is . - /// - /// - public bool IsRequired - { - get { return _isRequired; } - } + protected override bool CanSetProperty => false; - /// - /// Gets the default value for an argument. - /// - /// - /// The default value of the argument. - /// - /// - /// - /// The default value of an argument defined by a constructor parameter is specified by - /// the default value of that parameter. For an argument defined by a property, the default - /// value is set by the property. - /// - /// - /// This value is only used if is . - /// - /// - public object? DefaultValue - { - get { return _defaultValue; } + private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName) + { + return new ArgumentInfo() + { + Parser = parser, + ArgumentName = argumentName, + Kind = ArgumentKind.Method, + Long = true, + ArgumentType = typeof(bool), + ElementTypeWithNullable = typeof(bool), + ElementType = typeof(bool), + Description = parser.StringProvider.AutomaticVersionDescription(), + MemberName = nameof(AutomaticVersion), + Validators = Enumerable.Empty(), + Converter = Conversion.BooleanConverter.Instance + }; } - /// - /// Gets the description of the argument. - /// - /// - /// The description of the argument. - /// - /// - /// - /// This property is used only when generating usage information using . - /// - /// - /// To set the description of an argument, apply the - /// attribute to the constructor parameter, property, or method that defines the argument. - /// - /// - public string Description - { - get { return _description ?? string.Empty; } - } + protected override CancelMode CallMethod(object? value) => AutomaticVersion(Parser); - /// - /// Gets the short description of the argument's value to use when printing usage information. - /// - /// - /// The description of the value. - /// - /// - /// - /// The value description is a short, typically one-word description that indicates the type of value that - /// the user should supply. By default, the type of the property is used, applying the - /// specified by the property or the - /// property. If this is a - /// multi-value argument, the is used. If the type is a nullable - /// value type, its underlying type is used. - /// - /// - /// The value description is used only when generating usage help. For example, the usage for an argument named Sample with - /// a value description of String would look like "-Sample <String>". - /// - /// - /// This is not the long description used to describe the purpose of the argument. That can be retrieved - /// using the property. - /// - /// - /// - public string ValueDescription - { - get { return _valueDescription; } - } + protected override object? GetProperty(object target) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); - /// - /// Gets a value indicating whether this argument is a switch argument. - /// - /// - /// if the argument is a switch argument; otherwise, . - /// - /// - /// - /// A switch argument is an argument that doesn't need a value; instead, its value is or - /// depending on whether the argument is present on the command line. - /// - /// - /// A argument is a switch argument when it is not positional, and its - /// is a . - /// - /// - public bool IsSwitch => Position == null && ElementType == typeof(bool); - - /// - /// Gets a value which indicates what kind of argument this instance represents. - /// - /// - /// One of the values of the enumeration. - /// - /// - /// - /// An argument that is can accept multiple values - /// by being supplied more than once. An argument is multi-value if its - /// is an array or the argument was defined by a read-only property whose type implements - /// the generic interface. - /// - /// - /// An argument is dictionary argument is a - /// multi-value argument whose values are key/value pairs, which get added to a - /// dictionary based on the key. An argument is a dictionary argument when its - /// is , or it was defined - /// by a read-only property whose type implements the - /// property. - /// - /// - /// An argument is if it is backed by a method instead - /// of a property, which will be invoked when the argument is set. Method arguments - /// cannot be multi-value or dictionary arguments. - /// - /// - /// Otherwise, the value will be . - /// - /// - /// - public ArgumentKind Kind => _argumentKind; - - /// - /// Gets a value indicating whether this argument is a multi-value argument. - /// - /// - /// if the property is - /// or ; otherwise, . - /// - /// - /// - public bool IsMultiValue => _argumentKind is ArgumentKind.MultiValue or ArgumentKind.Dictionary; - - /// - /// Gets the separator for the values if this argument is a multi-value argument - /// - /// - /// The separator for multi-value arguments, or if no separator is used. - /// - /// - /// - /// If the property is , this property - /// is always . - /// - /// - /// - public string? MultiValueSeparator - { - get { return _multiValueSeparator; } - } + protected override void SetProperty(object target, object? value) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); - /// - /// Gets a value that indicates whether or not a multi-value argument can consume multiple - /// following argument values. - /// - /// - /// if a multi-value argument can consume multiple following values; - /// otherwise, . - /// - /// - /// - /// A multi-value argument that allows white-space separators is able to consume multiple - /// values from the command line that follow it. All values that follow the name, up until - /// the next argument name, are considered values for this argument. - /// - /// - /// If the property is , this property - /// is always . - /// - /// - /// - public bool AllowMultiValueWhiteSpaceSeparator => _allowMultiValueWhiteSpaceSeparator; - - /// - /// Gets the separator for key/value pairs if this argument is a dictionary argument. - /// - /// - /// The custom value specified using the attribute, or - /// if no attribute was present, or if this is not a dictionary argument. - /// - /// - /// - /// This property is only meaningful if the property is . - /// - /// - /// - public string? KeyValueSeparator => _keyValueSeparator; - - /// - /// Gets a value indicating whether this argument is a dictionary argument. - /// - /// - /// if this the property is ; - /// otherwise, . - /// - public bool IsDictionary => _argumentKind == ArgumentKind.Dictionary; - - /// - /// Gets a value indicating whether this argument, if it is a dictionary argument, allows duplicate keys. - /// - /// - /// if this argument allows duplicate keys; otherwise, . - /// - /// - /// - /// This property is only meaningful if the property is . - /// - /// - /// - public bool AllowsDuplicateDictionaryKeys - { - get { return _allowDuplicateDictionaryKeys; } - } + } - /// - /// Gets the value that the argument was set to in the last call to . - /// - /// - /// The value of the argument that was obtained when the command line arguments were parsed. - /// - /// - /// - /// The property provides an alternative method for accessing supplied argument - /// values, in addition to using the object returned by . - /// - /// - /// If an argument was supplied on the command line, the property will equal the - /// supplied value after conversion to the type specified by the property, - /// and the property will be . - /// - /// - /// If an optional argument was not supplied, the property will equal - /// the property, and will be . - /// - /// - /// If the property is , the property will - /// return an array with all the values, even if the argument type is a collection type rather than - /// an array. - /// - /// - /// If the property is , the property will - /// return a with all the values, even if the argument type is a different type. - /// - /// - public object? Value => _valueHelper?.Value; - - /// - /// Gets a value indicating whether the value of this argument was supplied on the command line in the last - /// call to . - /// - /// - /// if this argument's value was supplied on the command line when the arguments were parsed; otherwise, . - /// - /// - /// - /// Use this property to determine whether or not an argument was supplied on the command line, or was - /// assigned its default value. - /// - /// - /// When an optional argument is not supplied on the command line, the property will be equal - /// to the property, and will be . - /// - /// - /// It is however possible for the user to supply a value on the command line that matches the default value. - /// In that case, although the property will still be equal to the - /// property, the property will be . This allows you to distinguish - /// between an argument that was supplied or omitted even if the supplied value matches the default. - /// - /// - public bool HasValue { get; private set; } - - /// - /// Gets the name or alias that was used on the command line to specify this argument. - /// - /// - /// The name or alias that was used on the command line to specify this argument, or if this argument was specified by position or not specified. - /// - /// - /// - /// This property can be the value of the property, the property, - /// or any of the values in the and properties. - /// - /// - /// If the argument names are case-insensitive, the value of this property uses the casing as specified on the command line, not the original casing of the argument name or alias. - /// - /// - public string? UsedArgumentName { get; internal set; } - - /// - /// Gets a value that indicates whether or not this argument accepts values. - /// - /// - /// if the is a nullable reference type - /// or ; if the argument is any other - /// value type or, for .Net 6.0 and later only, a non-nullable reference type. - /// - /// - /// - /// For a multi-value argument, this value indicates whether the element type can be - /// . - /// - /// - /// For a dictionary argument, this value indicates whether the type of the dictionary's values can be - /// . Dictionary key types are always non-nullable, as this is a constraint on - /// . This works only if the argument type is - /// or . For other types that implement , - /// it is not possible to determine the nullability of TValue except if it's - /// a value type. - /// - /// - /// This property indicates what happens when the used for this argument returns - /// from its - /// method. - /// - /// - /// If this property is , the argument's value will be set to . - /// If it's , a will be thrown during - /// parsing with . - /// - /// - /// If the project containing the command line argument type does not use nullable reference types, or does - /// not support them (e.g. on older .Net versions), this property will only be for - /// value types other than . Only on .Net 6.0 and later will the property be - /// for non-nullable reference types. Although nullable reference types are available - /// on .Net Core 3.x, only .Net 6.0 and later will get this behavior due to the necessary runtime support to - /// determine nullability of a property or constructor argument. - /// - /// - public bool AllowNull => _allowNull; - - /// - /// Gets a value that indicates whether argument parsing should be canceled if this - /// argument is encountered. - /// - /// - /// if argument parsing should be canceled after this argument; - /// otherwise, . - /// - /// - /// - /// This value is determined using the - /// property. - /// - /// - /// If this property is , the will - /// stop parsing the command line arguments after seeing this argument, and return - /// from the method - /// or one of its overloads. Since no instance of the arguments type is returned, it's - /// not possible to determine argument values, or which argument caused the cancellation, - /// except by inspecting the property. - /// - /// - /// This property is most commonly useful to implement a "-Help" or "-?" style switch - /// argument, where the presence of that argument causes usage help to be printed and - /// the program to exit, regardless of whether the rest of the command line is valid - /// or not. - /// - /// - /// The method and the - /// static helper method - /// will print usage information if parsing was canceled through this method. - /// - /// - /// Canceling parsing in this way is identical to handling the - /// event and setting to . - /// - /// - /// It's possible to prevent cancellation when an argument has this property set by - /// handling the event and setting the - /// property to - /// . - /// - /// - public bool CancelParsing => _cancelParsing; - - /// - /// Gets or sets a value that indicates whether the argument is hidden from the usage help. - /// - /// - /// if the argument is hidden from the usage help; otherwise, - /// . The default value is . - /// - /// - /// - /// A hidden argument will not be included in the usage syntax or the argument description - /// list, even if is used. It does not - /// affect whether the argument can be used. - /// - /// - /// This property is always for positional or required arguments, - /// which may not be hidden. - /// - /// - public bool IsHidden => _isHidden; - - /// - /// Gets the argument validators applied to this argument. - /// - /// - /// A list of objects deriving from the class. - /// - public IEnumerable Validators => _validators; - - /// - /// Converts the specified string to the . - /// - /// The culture to use for conversion. - /// The string to convert. - /// The converted value. - /// - /// - /// Conversion is done by one of several methods. First, if a - /// was present on the constructor parameter, property, or method that defined the - /// property, the specified is used. Otherwise, if the - /// default for the can convert - /// from a string, it is used. Otherwise, a static Parse(, ) or - /// Parse() method on the type is used. Finally, a constructor that - /// takes a single parameter of type will be used. - /// - /// - /// - /// is - /// - /// - /// could not be converted to the type specified in the property. - /// - public object? ConvertToArgumentType(CultureInfo culture, string? argumentValue) - { - if (culture == null) - { - throw new ArgumentNullException(nameof(culture)); - } + internal struct ArgumentInfo + { + public CommandLineParser Parser { get; set; } + public string MemberName { get; set; } + public string ArgumentName { get; set; } + public bool Long { get; set; } + public bool Short { get; set; } + public char ShortName { get; set; } + public IEnumerable? Aliases { get; set; } + public IEnumerable? ShortAliases { get; set; } + public Type ArgumentType { get; set; } + public Type ElementType { get; set; } + public Type ElementTypeWithNullable { get; set; } + public ArgumentKind Kind { get; set; } + public ArgumentConverter Converter { get; set; } + public int? Position { get; set; } + public bool IsRequired { get; set; } + public bool IsRequiredProperty { get; set; } + public object? DefaultValue { get; set; } + public bool IncludeDefaultValueInHelp { get; set; } + public string? Description { get; set; } + public string? ValueDescription { get; set; } + public bool AllowNull { get; set; } + public CancelMode CancelParsing { get; set; } + public bool IsHidden { get; set; } + public IEnumerable Validators { get; set; } + public MultiValueArgumentInfo? MultiValueInfo { get; set; } + public DictionaryArgumentInfo? DictionaryInfo { get; set; } + } - if (argumentValue == null) + #endregion + + private readonly CommandLineParser _parser; + private readonly ArgumentConverter _converter; + private readonly string _argumentName; + private readonly bool _hasLongName = true; + private readonly char _shortName; + private readonly ImmutableArray _aliases = ImmutableArray.Empty; + private readonly ImmutableArray _shortAliases = ImmutableArray.Empty; + private readonly Type _argumentType; + private readonly Type _elementType; + 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; + private readonly CancelMode _cancelParsing; + private readonly bool _isHidden; + private readonly IEnumerable _validators; + private string? _valueDescription; + private IValueHelper? _valueHelper; + private ReadOnlyMemory _usedArgumentName; + + 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; + _argumentName = info.ArgumentName; + if (_parser.Mode == ParsingMode.LongShort) + { + _hasLongName = info.Long; + if (info.Short) { - if (IsSwitch) + if (info.ShortName != '\0') { - return true; + _shortName = info.ShortName; } else { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingNamedArgumentValue, this); + _shortName = _argumentName[0]; } } - try + if (!HasLongName) { - var converted = _converter.ConvertFrom(null, culture, argumentValue); - if (converted == null && (!_allowNull || IsDictionary)) + if (!HasShortName) { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoLongOrShortName, _argumentName)); } - return converted; - } - catch (NotSupportedException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, argumentValue); - } - catch (FormatException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, argumentValue); - } - catch (Exception ex) - { - // Yeah, I don't like catching Exception, but unfortunately BaseNumberConverter (e.g. used for int) can *throw* a System.Exception (not a derived class) so there's nothing I can do about it. - if (ex.InnerException is FormatException) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, argumentValue); - } - else - { - throw; - } + _argumentName = _shortName.ToString(); } } - /// - /// Converts any type to the argument's . - /// - /// The value to convert. - /// The converted value. - /// - /// The argument's cannot convert between the type of - /// and the . - /// - /// - /// - /// If the type of is directly assignable to , - /// no conversion is done. If the is a , - /// the same rules apply as for the - /// method, using . Otherwise, the - /// for the argument is used to convert between the source. - /// - /// - /// This method is used to convert the - /// property to the correct type, and is also used by implementations of the - /// class to convert values when needed. - /// - /// - /// The conversion is not supported. - public object? ConvertToArgumentTypeInvariant(object? value) - { - if (value == null || _elementTypeWithNullable.IsAssignableFrom(value.GetType())) - { - return value; - } - - return _converter.ConvertFrom(null, CultureInfo.InvariantCulture, value); + if (HasLongName && info.Aliases != null) + { + _aliases = info.Aliases.ToImmutableArray(); } - /// - /// Returns a that represents the current . - /// - /// A that represents the current . - /// - /// - /// The string value matches the way the argument is displayed in the usage help's command line syntax - /// when using the default . - /// - /// - public override string ToString() - { - return (new UsageWriter()).GetArgumentUsage(this); + if (HasShortName && info.ShortAliases != null) + { + _shortAliases = info.ShortAliases.ToImmutableArray(); } - internal bool HasInformation(UsageWriter writer) + _argumentType = info.ArgumentType; + _argumentKind = info.Kind; + _elementTypeWithNullable = info.ElementTypeWithNullable; + _elementType = info.ElementType; + _description = info.Description; + _isRequired = info.IsRequired; + IsRequiredProperty = info.IsRequiredProperty; + _allowNull = info.AllowNull; + _cancelParsing = info.CancelParsing; + _validators = info.Validators; + // Required or positional arguments cannot be hidden. + _isHidden = info.IsHidden && !info.IsRequired && info.Position == null; + Position = info.Position; + _converter = info.Converter; + _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); + IncludeDefaultInUsageHelp = info.IncludeDefaultValueInHelp; + _valueDescription = info.ValueDescription; + _allowNull = info.AllowNull; + DictionaryInfo = info.DictionaryInfo; + MultiValueInfo = info.MultiValueInfo; + if (MultiValueInfo != null && IsSwitch) { - if (!string.IsNullOrEmpty(Description)) - { - return true; - } + MultiValueInfo.AllowWhiteSpaceSeparator = false; + } + } - if (writer.UseAbbreviatedSyntax && Position == null) - { - return true; - } + /// + /// Gets the that this argument belongs to. + /// + /// + /// An instance of the class. + /// + public CommandLineParser Parser => _parser; - if (writer.UseShortNamesForSyntax) - { - if (HasLongName) - { - return true; - } - } - else if (HasShortName) - { - return true; - } + /// + /// Gets the name of the property or method that defined this command line argument. + /// + /// + /// The name of the property or method that defined this command line argument. + /// + public string MemberName + { + get { return _memberName; } + } - if (writer.IncludeAliasInDescription && - ((Aliases != null && Aliases.Count > 0) || (ShortAliases != null && ShortAliases.Count > 0))) - { - return true; - } + /// + /// Gets the name of this argument. + /// + /// + /// The name of this argument. + /// + /// + /// + /// This name is used to supply an argument value by name on the command line, and to describe the argument in the usage help + /// generated by . + /// + /// + /// If the property is , + /// and the property is , this returns + /// the short name of the argument. Otherwise, it returns the long name. + /// + /// + /// + public string ArgumentName => _argumentName; - if (writer.IncludeDefaultValueInDescription && DefaultValue != null) - { - return true; - } + /// + /// Gets the short name of this argument. + /// + /// + /// The short name of the argument, or a null character ('\0') if it doesn't have one. + /// + /// + /// + /// The short name is only used if the parser is using . + /// + /// + /// + public char ShortName => _shortName; - if (writer.IncludeValidatorsInDescription && - _validators.Any(v => !string.IsNullOrEmpty(v.GetUsageHelp(this)))) - { - return true; - } + /// + /// Gets the name of this argument, with the appropriate argument name prefix. + /// + /// + /// The name of the argument, with an argument name prefix. + /// + /// + /// + /// If the property is , + /// this will use the long name with the long argument prefix if the argument has a long + /// name, and the short name with the primary short argument prefix if not. + /// + /// + /// For , the prefix used is the first prefix specified + /// in the property. + /// + /// + public string ArgumentNameWithPrefix + { + get + { + var prefix = (_parser.Mode == ParsingMode.LongShort && HasLongName) + ? _parser.LongArgumentNamePrefix + : _parser.ArgumentNamePrefixes[0]; - return false; + return prefix + _argumentName; } + } - internal bool SetValue(CultureInfo culture, string? value) + /// + /// Gets the long argument name with the long prefix. + /// + /// + /// The long argument name with its prefix, or if the + /// property is not or the + /// property is . + /// + public string? LongNameWithPrefix + { + get { - _valueHelper ??= CreateValueHelper(); + return (_parser.Mode == ParsingMode.LongShort && HasLongName) + ? _parser.LongArgumentNamePrefix + _argumentName + : null; + } + } - bool continueParsing; - if (IsMultiValue && value != null && MultiValueSeparator != null) - { - continueParsing = true; - string[] values = value.Split(new[] { MultiValueSeparator }, StringSplitOptions.None); - foreach (string separateValue in values) - { - Validate(separateValue, ValidationMode.BeforeConversion); - var converted = ConvertToArgumentType(culture, separateValue); - continueParsing = _valueHelper.SetValue(this, culture, converted); - if (!continueParsing) - { - break; - } + /// + /// Gets the short argument name with the primary short prefix. + /// + /// + /// The short argument name with its prefix, or if the + /// property is not or the + /// property is . + /// + /// + /// + /// The prefix used is the first prefix specified in the + /// property. + /// + /// + public string? ShortNameWithPrefix + { + get + { + return (_parser.Mode == ParsingMode.LongShort && HasShortName) + ? _parser.ArgumentNamePrefixes[0] + _shortName + : null; + } + } - Validate(converted, ValidationMode.AfterConversion); - } - } - else - { - Validate(value, ValidationMode.BeforeConversion); - var converted = ConvertToArgumentType(culture, value); - continueParsing = _valueHelper.SetValue(this, culture, converted); - Validate(converted, ValidationMode.AfterConversion); - } + /// + /// Gets a value that indicates whether the argument has a short name. + /// + /// + /// if the argument has a short name; otherwise, . + /// + /// + /// + /// The short name is only used if the parser is using . + /// Otherwise, this property is always . + /// + /// + /// + public bool HasShortName => _shortName != '\0'; - HasValue = true; - return continueParsing; - } + /// + /// Gets a value that indicates whether the argument has a long name. + /// + /// + /// if the argument has a long name; otherwise, . + /// + /// + /// + /// If the property is not , + /// this property is always . + /// + /// + /// + public bool HasLongName => _hasLongName; - internal static CommandLineArgument Create(CommandLineParser parser, ParameterInfo parameter) - { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + /// + /// Gets the alternative names for this command line argument. + /// + /// + /// A list of alternative names for this command line argument, or an empty array if none were specified. + /// + /// + /// + /// If the property is , + /// and the property is , this property + /// will always return an empty array. + /// + /// + /// + public ImmutableArray Aliases => _aliases; - if (parameter?.Name == null) - { - throw new ArgumentNullException(nameof(parameter)); - } + /// + /// Gets the alternative short names for this command line argument. + /// + /// + /// A list of alternative short names for this command line argument, or an empty array if none + /// were specified. + /// + /// + /// + /// If the property is not , + /// or the property is , this property + /// will always return an empty array. + /// + /// + /// + public ImmutableArray ShortAliases => _shortAliases; - var typeConverterAttribute = parameter.GetCustomAttribute(); - var keyTypeConverterAttribute = parameter.GetCustomAttribute(); - var valueTypeConverterAttribute = parameter.GetCustomAttribute(); - var argumentNameAttribute = parameter.GetCustomAttribute(); - var multiValueSeparatorAttribute = parameter.GetCustomAttribute(); - var argumentName = DetermineArgumentName(argumentNameAttribute?.ArgumentName, parameter.Name, parser.Options.ArgumentNameTransform); - var info = new ArgumentInfo() - { - Parser = parser, - Parameter = parameter, - ArgumentName = argumentName, - Long = argumentNameAttribute?.IsLong ?? true, - Short = argumentNameAttribute?.IsShort ?? false, - ShortName = argumentNameAttribute?.ShortName ?? '\0', - ArgumentType = parameter.ParameterType, - Description = parameter.GetCustomAttribute()?.Description, - DefaultValue = (parameter.Attributes & ParameterAttributes.HasDefault) == ParameterAttributes.HasDefault ? parameter.DefaultValue : null, - ValueDescription = parameter.GetCustomAttribute()?.ValueDescription, - AllowDuplicateDictionaryKeys = Attribute.IsDefined(parameter, typeof(AllowDuplicateDictionaryKeysAttribute)), - ConverterType = typeConverterAttribute == null ? null : Type.GetType(typeConverterAttribute.ConverterTypeName, true), - KeyConverterType = keyTypeConverterAttribute == null ? null : Type.GetType(keyTypeConverterAttribute.ConverterTypeName, true), - ValueConverterType = valueTypeConverterAttribute == null ? null : Type.GetType(valueTypeConverterAttribute.ConverterTypeName, true), - MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), - AllowMultiValueWhiteSpaceSeparator = multiValueSeparatorAttribute != null && multiValueSeparatorAttribute.Separator == null, - KeyValueSeparator = parameter.GetCustomAttribute()?.Separator, - Aliases = GetAliases(parameter.GetCustomAttributes(), argumentName), - ShortAliases = GetShortAliases(parameter.GetCustomAttributes(), argumentName), - Position = parameter.Position, - IsRequired = !parameter.IsOptional, - MemberName = parameter.Name, - AllowNull = DetermineAllowsNull(parameter), - Validators = parameter.GetCustomAttributes(), - }; + /// + /// Gets the type of the argument's value. + /// + /// + /// The of the argument. + /// + public Type ArgumentType + { + get { return _argumentType; } + } - return new CommandLineArgument(info); - } + /// + /// Gets the type of the elements of the argument value. + /// + /// + /// If the property is , + /// the of each individual value; if it is , + /// ; if the argument type is , + /// the type T; otherwise, the same value as the + /// property. + /// + public Type ElementType => _elementType; - internal static CommandLineArgument Create(CommandLineParser parser, PropertyInfo property) - { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + /// + /// Gets the converter used to convert string values to the argument's type. + /// + /// + /// The for this argument. + /// + public ArgumentConverter Converter => _converter; - if (property == null) - { - throw new ArgumentNullException(nameof(property)); - } + /// + /// Gets the position of this argument. + /// + /// + /// The position of this argument, or if this is not a positional argument. + /// + /// + /// + /// A positional argument is created by using the + /// or property. + /// + /// + /// The property reflects the actual position of the positional argument. + /// This doesn't need to match the original value of the + /// property. + /// + /// + public int? Position { get; internal set; } - return Create(parser, property, null, property.PropertyType, DetermineAllowsNull(property)); - } + /// + /// Gets a value that indicates whether the argument is required. + /// + /// + /// if the argument's value must be specified on the command line; + /// if the argument may be omitted. + /// + /// + /// + /// An argument is required if its , + /// property is , or if it was defined by an property with the + /// required keyword available in C# 11 and later. + /// + /// + public bool IsRequired + { + get { return _isRequired; } + } - internal static CommandLineArgument Create(CommandLineParser parser, MethodInfo method) - { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + /// + /// Gets a value that indicates whether the argument is backed by a required property. + /// + /// + /// if the argument is defined by a property with the C# 11 + /// required keyword; otherwise, . + /// + /// + /// + /// If the property is , the + /// property is guaranteed to also be . + /// + /// + public bool IsRequiredProperty { get; } - if (method == null) - { - throw new ArgumentNullException(nameof(method)); - } + /// + /// Gets the default value for an argument. + /// + /// + /// The default value of the argument. + /// + /// + /// + /// The default value is set by the + /// property, or when the is used it can also be + /// specified using a property initializer. + /// + /// + /// This value is only used if the property is . + /// + /// + public object? DefaultValue + { + get { return _defaultValue; } + } - var infoTuple = DetermineMethodArgumentInfo(method); - if (infoTuple == null) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.InvalidMethodSignatureFormat, method.Name)); - } + /// + /// Gets a value that indicates whether the default value should be included in the argument's + /// description in the usage help. + /// + /// + /// if the default value should be shown in the usage help; otherwise, + /// . + /// + /// + /// + /// This value is set by the + /// property. + /// + /// + /// The default value will only be shown if the property is not + /// , and if both this property and the + /// property are . + /// + /// + public bool IncludeDefaultInUsageHelp { get; } - var (methodInfo, argumentType, allowsNull) = infoTuple.Value; - return Create(parser, null, methodInfo, argumentType, allowsNull); - } + /// + /// Gets the description of the argument. + /// + /// + /// The description of the argument. + /// + /// + /// + /// This property is used only when generating usage information using . + /// + /// + /// To set the description of an argument, apply the + /// attribute to the property or method that defines the argument. + /// + /// + public string Description + { + get { return _description ?? string.Empty; } + } - private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo? property, MethodArgumentInfo? method, - Type argumentType, bool allowsNull) - { - var member = ((MemberInfo?)property ?? method?.Method)!; - var attribute = member.GetCustomAttribute(); - if (attribute == null) - { - throw new ArgumentException(Properties.Resources.MissingArgumentAttribute, nameof(method)); - } + /// + /// Gets the short description of the argument's value to use when printing usage information. + /// + /// + /// The description of the value. + /// + /// + /// + /// The value description is a short, typically one-word description that indicates the type + /// of value that the user should supply. By default, the type of the property is used, + /// applying the specified by the + /// property or the + /// property. If this is a multi-value argument or the argument's type is , + /// the is used. + /// + /// + /// The value description is used when generating usage help. For example, the usage for an + /// argument named Sample with a value description of String would look like "-Sample + /// <String>". + /// + /// + /// This is not the long description used to describe the purpose of the argument. That can be + /// retrieved using the property. + /// + /// + /// + /// + public string ValueDescription => _valueDescription ??= DetermineValueDescription(); - var typeConverterAttribute = member.GetCustomAttribute(); - var keyTypeConverterAttribute = member.GetCustomAttribute(); - var valueTypeConverterAttribute = member.GetCustomAttribute(); - var multiValueSeparatorAttribute = member.GetCustomAttribute(); - var argumentName = DetermineArgumentName(attribute.ArgumentName, member.Name, parser.Options.ArgumentNameTransform); - var info = new ArgumentInfo() - { - Parser = parser, - Property = property, - Method = method, - ArgumentName = argumentName, - Long = attribute.IsLong, - Short = attribute.IsShort, - ShortName = attribute.ShortName, - ArgumentType = argumentType, - Description = member.GetCustomAttribute()?.Description, - ValueDescription = attribute.ValueDescription, // If null, the constructor will sort it out. - Position = attribute.Position < 0 ? null : attribute.Position, - AllowDuplicateDictionaryKeys = Attribute.IsDefined(member, typeof(AllowDuplicateDictionaryKeysAttribute)), - ConverterType = typeConverterAttribute == null ? null : Type.GetType(typeConverterAttribute.ConverterTypeName, true), - KeyConverterType = keyTypeConverterAttribute == null ? null : Type.GetType(keyTypeConverterAttribute.ConverterTypeName, true), - ValueConverterType = valueTypeConverterAttribute == null ? null : Type.GetType(valueTypeConverterAttribute.ConverterTypeName, true), - MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), - AllowMultiValueWhiteSpaceSeparator = multiValueSeparatorAttribute != null && multiValueSeparatorAttribute.Separator == null, - KeyValueSeparator = member.GetCustomAttribute()?.Separator, - Aliases = GetAliases(member.GetCustomAttributes(), argumentName), - ShortAliases = GetShortAliases(member.GetCustomAttributes(), argumentName), - DefaultValue = attribute.DefaultValue, - IsRequired = attribute.IsRequired, - MemberName = member.Name, - AllowNull = allowsNull, - CancelParsing = attribute.CancelParsing, - IsHidden = attribute.IsHidden, - Validators = member.GetCustomAttributes(), - }; + /// + /// Gets a value indicating whether this argument is a switch argument. + /// + /// + /// if the argument is a switch argument; otherwise, . + /// + /// + /// + /// A switch argument is an argument that doesn't need a value; instead, its value is or + /// depending on whether the argument is present on the command line. + /// + /// + /// A argument is a switch argument when it is not positional, and its + /// is a . + /// + /// + public bool IsSwitch => Position == null && ElementType == typeof(bool); - return new CommandLineArgument(info); - } + /// + /// Gets a value which indicates what kind of argument this instance represents. + /// + /// + /// One of the values of the enumeration. + /// + /// + /// + /// An argument that is can accept multiple values + /// by being supplied more than once. An argument is multi-value if its + /// is an array or the argument was defined by a read-only property whose type implements + /// the generic interface. + /// + /// + /// An argument that is is a + /// multi-value argument whose values are key/value pairs, which get added to a + /// dictionary based on the key. An argument is a dictionary argument when its + /// is , or it was defined + /// by a read-only property whose type implements the + /// interface. + /// + /// + /// An argument is if it is backed by a method instead + /// of a property, which will be invoked when the argument is set. Method arguments + /// cannot be multi-value or dictionary arguments. + /// + /// + /// Otherwise, the value will be . + /// + /// + public ArgumentKind Kind => _argumentKind; - internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser, IDictionary? defaultValueDescriptions, NameTransform valueDescriptionTransform) - { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + /// + /// Gets information that only applies to multi-value or dictionary arguments. + /// + /// + /// An instance of the class, or + /// if the property is not + /// or . + /// + /// + /// + /// For dictionary arguments, this property only returns the information that apples 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. + /// + /// + public MultiValueArgumentInfo? MultiValueInfo { get; } - var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticHelpName(), parser.Options.ArgumentNameTransform); - var shortName = parser.StringProvider.AutomaticHelpShortName(); - var shortAlias = char.ToLowerInvariant(argumentName[0]); - var existingArg = parser.GetArgument(argumentName) ?? - (parser.Mode == ParsingMode.LongShort - ? (parser.GetShortArgument(shortName) ?? parser.GetShortArgument(shortAlias)) - : (parser.GetArgument(shortName.ToString()) ?? parser.GetArgument(shortAlias.ToString()))); + /// + /// Gets information that only applies to dictionary arguments. + /// + /// + /// An instance of the class, or + /// if the property is not . + /// + /// + /// + /// Since dictionary arguments are a type of multi-value argument, also see the + /// property. + /// + /// + public DictionaryArgumentInfo? DictionaryInfo { get; } - if (existingArg != null) - { - return (existingArg, false); - } + /// + /// Gets the value that the argument was set to in the last call to . + /// + /// + /// The value of the argument that was obtained when the command line arguments were parsed. + /// + /// + /// + /// The property provides an alternative method for accessing supplied argument + /// values, in addition to using the object returned by . + /// + /// + /// If an argument was supplied on the command line, the property will equal the + /// supplied value after conversion to the type specified by the property, + /// and the property will be . + /// + /// + /// If an optional argument was not supplied, the property will equal + /// the property, and will be . + /// + /// + /// If the property is , the property will + /// return an array with all the values, even if the argument type is a collection type rather than + /// an array. + /// + /// + /// If the property is , the property will + /// return a with all the values, even if the argument type is a different type. + /// + /// + public object? Value => _valueHelper?.Value; - var memberName = nameof(AutomaticHelp); - var info = new ArgumentInfo() - { - Parser = parser, - Method = new() - { - Method = typeof(CommandLineArgument).GetMethod(memberName, BindingFlags.NonPublic | BindingFlags.Static)!, - }, - ArgumentName = argumentName, - Long = true, - Short = true, - ShortName = parser.StringProvider.AutomaticHelpShortName(), - ArgumentType = typeof(bool), - Description = parser.StringProvider.AutomaticHelpDescription(), - MemberName = memberName, - CancelParsing = true, - Validators = Enumerable.Empty(), - }; + /// + /// Gets a value indicating whether the value of this argument was supplied on the command line in the last + /// call to . + /// + /// + /// if this argument's value was supplied on the command line when the arguments were parsed; otherwise, . + /// + /// + /// + /// Use this property to determine whether or not an argument was supplied on the command line, or was + /// assigned its default value. + /// + /// + /// When an optional argument is not supplied on the command line, the property will be equal + /// to the property, and will be . + /// + /// + /// It is however possible for the user to supply a value on the command line that matches the default value. + /// In that case, although the property will still be equal to the + /// property, the property will be . This allows you to distinguish + /// between an argument that was supplied or omitted even if the supplied value matches the default. + /// + /// + public bool HasValue { get; private set; } - var shortNameString = shortName.ToString(); - var shortAliasString = shortAlias.ToString(); - if (parser.Mode == ParsingMode.LongShort) - { - if (parser.ArgumentNameComparer.Compare(shortAliasString, shortNameString) != 0) - { - info.ShortAliases = new[] { shortAlias }; - } - } - else - { - info.Aliases = parser.ArgumentNameComparer.Compare(shortAliasString, shortNameString) == 0 - ? new[] { shortNameString } - : new[] { shortNameString, shortAliasString }; - } + /// + /// Gets the name or alias that was used on the command line to specify this argument. + /// + /// + /// The name or alias that was used on the command line to specify this argument, or + /// if this argument was specified by position or not specified. + /// + /// + /// + /// This property can be the value of the property, the + /// property, or any of the values in the and + /// properties. Unless disabled using the + /// or property, it + /// can also be any unique prefix of an argument name or alias. + /// + /// + /// If the argument names are case-insensitive, the value of this property uses the casing as + /// specified on the command line, not the original casing of the argument name or alias. + /// + /// + public string? UsedArgumentName => _usedArgumentName.Length > 0 ? _usedArgumentName.ToString() : null; + + /// + /// Gets a value that indicates whether or not this argument accepts values. + /// + /// + /// if the property is a nullable reference + /// type or the property is ; + /// if the argument's type any other value type or, for .Net 6.0 and + /// later only, a non-nullable reference type. + /// + /// + /// + /// For a multi-value argument, this value indicates whether the element type can be + /// . + /// + /// + /// For a dictionary argument, this value indicates whether the type of the dictionary's values can be + /// . Dictionary key types are always non-nullable, as this is a constraint on + /// . This works only if the argument type is + /// or , or if source generation was used. For other + /// types that implement , it is not possible to + /// determine the nullability of TValue at runtime except if it's a value type. + /// + /// + /// This property indicates what happens when the + /// method used for this argument returns . + /// + /// + /// If this property is , the argument's value will be set to . + /// If it's , a will be thrown during + /// parsing with . + /// + /// + /// If the project containing the command line argument type does not use nullable reference + /// types, or does not support them (e.g. on older .Net versions), this property will only be + /// for value types other than . Only on + /// .Net 6.0 and later, or if source generation was used with the , + /// attribute will the property be for non-nullable reference types. + /// Although nullable reference types are available on .Net Core 3.x, only .Net 6.0 and later + /// will get this behavior without source generation due to the necessary runtime support to + /// determine nullability of a property or method parameter. + /// + /// + public bool AllowNull => _allowNull; + + /// + /// Gets a value that indicates whether argument parsing should be canceled if this + /// argument is encountered. + /// + /// + /// One of the values of the enumeration. + /// + /// + /// + /// This value is determined using the + /// property. + /// + /// + public CancelMode CancelParsing => _cancelParsing; + + /// + /// Gets or sets a value that indicates whether the argument is hidden from the usage help. + /// + /// + /// if the argument is hidden from the usage help; otherwise, + /// . The default value is . + /// + /// + /// + /// A hidden argument will not be included in the usage syntax or the argument description + /// list, even if is used. It does not + /// affect whether the argument can be used. + /// + /// + /// This property is always for positional or required arguments, + /// which may not be hidden. + /// + /// + public bool IsHidden => _isHidden; + + /// + /// Gets the argument validators applied to this argument. + /// + /// + /// A list of objects deriving from the class. + /// + public IEnumerable Validators => _validators; + + /// + /// When implemented in a derived class, gets a value that indicates whether this argument + /// is backed by a property with a public set method. + /// + /// + /// if this argument's value will be stored in a writable property; + /// otherwise, . + /// + protected abstract bool CanSetProperty { get; } - return (new CommandLineArgument(info), true); + /// + /// Converts the specified string to the . + /// + /// The culture to use for conversion. + /// The string to convert. + /// The converted value. + /// + /// + /// Conversion is done by one of several methods. First, if a was present on the property or method that + /// defined the argument, the specified is used. + /// Otherwise, the type must implement , implement + /// , or have a static Parse(, + /// ) or Parse() method, or have a + /// constructor that takes a single parameter of type . + /// + /// + /// + /// is + /// + /// + /// could not be converted to the type specified in the + /// property. + /// + public object? ConvertToArgumentType(CultureInfo culture, string? argumentValue) + => ConvertToArgumentType(culture, argumentValue != null, argumentValue, argumentValue.AsSpan()); + + /// + /// Converts any type to the argument's . + /// + /// The value to convert. + /// The converted value. + /// + /// + /// If the type of is directly assignable to , + /// no conversion is done. If the is a , + /// the same rules apply as for the + /// method, using . Other types + /// will be converted to a string before conversion. + /// + /// + /// This method is used to convert the + /// property to the correct type, and is also used by implementations of the + /// class to convert values when needed. + /// + /// + /// + /// The argument's cannot convert between the type of + /// and the . + /// + public object? ConvertToArgumentTypeInvariant(object? value) + { + if (value == null || _elementTypeWithNullable.IsAssignableFrom(value.GetType())) + { + return value; } - internal static CommandLineArgument? CreateAutomaticVersion(CommandLineParser parser, IDictionary? defaultValueDescriptions, NameTransform valueDescriptionTransform) + var stringValue = value.ToString(); + if (stringValue == null) { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + return null; + } + + return _converter.Convert(stringValue, CultureInfo.InvariantCulture, this); + } + + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + /// + /// + /// The string value matches the way the argument is displayed in the usage help's command line syntax + /// when using the default . + /// + /// + public override string ToString() + { + return (new UsageWriter()).GetArgumentUsage(this); + } + + /// + /// When implemented in a derived class, sets the property for this argument. + /// + /// An instance of the type that defined the argument. + /// The value of the argument. + /// + /// This argument does not use a writable property. + /// + protected abstract void SetProperty(object target, object? value); + + /// + /// When implemented in a derived class, gets the value of the property for this argument. + /// + /// An instance of the type that defined the argument. + /// The value of the property + /// + /// This argument does not use a property. + /// + protected abstract object? GetProperty(object target); - var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticVersionName(), parser.Options.ArgumentNameTransform); - if (parser.GetArgument(argumentName) != null) - { - return null; - } + /// + /// When implemented in a derived class, calls the method that defined the property. + /// + /// The argument value. + /// The return value of the argument's method. + /// + /// This argument does not use a method. + /// + protected abstract CancelMode CallMethod(object? value); - var memberName = nameof(AutomaticVersion); - var info = new ArgumentInfo() - { - Parser = parser, - Method = new() - { - Method = typeof(CommandLineArgument).GetMethod(memberName, BindingFlags.NonPublic | BindingFlags.Static)!, - HasParserParameter = true, - }, - ArgumentName = argumentName, - Long = true, - ArgumentType = typeof(bool), - Description = parser.StringProvider.AutomaticVersionDescription(), - MemberName = memberName, - Validators = Enumerable.Empty(), - }; + /// + /// Determines the value description if one wasn't explicitly given. + /// + /// + /// The type to get the description for. + /// + /// The value description. + protected virtual string DetermineValueDescriptionForType(Type type) => GetFriendlyTypeName(type); + + internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, + Type argumentType, + bool allowsNull, + bool requiredProperty, + string memberName, + CommandLineArgumentAttribute attribute, + DescriptionAttribute? descriptionAttribute, + ValueDescriptionAttribute? valueDescriptionAttribute, + IEnumerable? aliasAttributes, + IEnumerable? shortAliasAttributes, + IEnumerable? validationAttributes) + { + var argumentName = DetermineArgumentName(attribute.ArgumentName, memberName, parser.Options.ArgumentNameTransformOrDefault); + return new ArgumentInfo() + { + Parser = parser, + ArgumentName = argumentName, + Long = attribute.IsLong, + Short = attribute.IsShort, + ShortName = attribute.ShortName, + ArgumentType = argumentType, + ElementTypeWithNullable = argumentType, + Description = descriptionAttribute?.Description, + ValueDescription = valueDescriptionAttribute?.ValueDescription, + Position = attribute.Position < 0 ? null : attribute.Position, + Aliases = GetAliases(aliasAttributes, argumentName), + ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), + DefaultValue = attribute.DefaultValue, + IncludeDefaultValueInHelp = attribute.IncludeDefaultInUsageHelp, + IsRequired = attribute.IsRequired || requiredProperty, + IsRequiredProperty = requiredProperty, + MemberName = memberName, + AllowNull = allowsNull, + CancelParsing = attribute.CancelParsing, + IsHidden = attribute.IsHidden, + Validators = validationAttributes ?? Enumerable.Empty(), + }; + } - return new CommandLineArgument(info); + private string DetermineValueDescription(Type? type = null) + { + var result = GetDefaultValueDescription(type); + if (result != null) + { + return result; } - internal object? GetConstructorParameterValue() + if (type == null && DictionaryInfo != null) { - return Value; + var key = DetermineValueDescription(DictionaryInfo.KeyType.GetUnderlyingType()); + var value = DetermineValueDescription(DictionaryInfo.ValueType.GetUnderlyingType()); + return $"{key}{DictionaryInfo.KeyValueSeparator}{value}"; } - internal void ApplyPropertyValue(object target) - { - // Do nothing for parameter-based values - if (_property == null) - { - return; - } + var typeName = DetermineValueDescriptionForType(type ?? ElementType); + return Parser.Options.ValueDescriptionTransformOrDefault.Apply(typeName); + } - try - { - if (_valueHelper != null) + private static string GetFriendlyTypeName(Type type) + { + // This is used to generate a value description from a type name if no custom value description was supplied. + if (type.IsGenericType) + { + var name = new StringBuilder(type.FullName?.Length ?? 0); + name.Append(type.Name, 0, type.Name.IndexOf("`", StringComparison.Ordinal)); + name.Append('<'); + // AppendJoin is not supported in .Net Standard 2.0 + bool first = true; + foreach (Type typeArgument in type.GetGenericArguments()) + { + if (first) { - _valueHelper.ApplyValue(target, _property); + first = false; + } + else + { + name.Append(", "); } - } - catch (TargetInvocationException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, this); - } - } - internal void Reset() - { - if (!IsMultiValue && _defaultValue != null) - { - _valueHelper = new SingleValueHelper(_defaultValue); - } - else - { - _valueHelper = null; + name.Append(GetFriendlyTypeName(typeArgument)); } - HasValue = false; - UsedArgumentName = null; + name.Append('>'); + return name.ToString(); } - - internal static void ShowVersion(LocalizedStringProvider stringProvider, Assembly assembly, string friendlyName) + else { - Console.WriteLine(stringProvider.ApplicationNameAndVersion(assembly, friendlyName)); - var copyright = stringProvider.ApplicationCopyright(assembly); - if (copyright != null) - { - Console.WriteLine(copyright); - } + return type.Name; } + } - internal void ValidateAfterParsing() + internal object? ConvertToArgumentType(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + { + if (culture == null) { - if (HasValue) - { - Validate(null, ValidationMode.AfterParsing); - } - else if (IsRequired) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingRequiredArgument, ArgumentName); - } + throw new ArgumentNullException(nameof(culture)); } - private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) + if (!hasValue) { - var separator = attribute?.Separator; - if (string.IsNullOrEmpty(separator)) + if (IsSwitch) { - return null; + return true; } else { - return separator; + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingNamedArgumentValue, this); } } - private static string GetFriendlyTypeName(Type type) + try { - // This is used to generate a value description from a type name if no custom value description was supplied. - if (type.IsGenericType) - { - var name = new StringBuilder(type.FullName?.Length ?? 0); - name.Append(type.Name, 0, type.Name.IndexOf("`", StringComparison.Ordinal)); - name.Append('<'); - // If only I was targeting .Net 4, I could use string.Join for this. - bool first = true; - foreach (Type typeArgument in type.GetGenericArguments()) - { - if (first) - { - first = false; - } - else - { - name.Append(", "); - } - - name.Append(GetFriendlyTypeName(typeArgument)); - } + var converted = stringValue == null + ? _converter.Convert(spanValue, culture, this) + : _converter.Convert(stringValue, culture, this); - name.Append('>'); - return name.ToString(); - } - else + if (converted == null && (!_allowNull || Kind == ArgumentKind.Dictionary)) { - return type.Name; + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); } - } - private IValueHelper CreateValueHelper() + return converted; + } + catch (CommandLineArgumentException ex) { - Debug.Assert(_valueHelper == null); - Type type; - switch (_argumentKind) + if (ex.ArgumentName == ArgumentName) { - case ArgumentKind.Dictionary: - type = typeof(DictionaryValueHelper<,>).MakeGenericType(_elementType.GetGenericArguments()); - return (IValueHelper)Activator.CreateInstance(type, _allowDuplicateDictionaryKeys, _allowNull)!; + throw; + } - case ArgumentKind.MultiValue: - type = typeof(MultiValueHelper<>).MakeGenericType(_elementTypeWithNullable); - return (IValueHelper)Activator.CreateInstance(type)!; + // Patch with the correct argument name. + throw new CommandLineArgumentException(ex.Message, ArgumentName, ex.Category, ex); + } + catch (Exception ex) + { + // Wrap any other exception in a CommandLineArgumentException. + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + } + } - case ArgumentKind.Method: - return new MethodValueHelper(); + internal bool HasInformation(UsageWriter writer) + { + if (!string.IsNullOrEmpty(Description)) + { + return true; + } - default: - Debug.Assert(_defaultValue == null); - return new SingleValueHelper(null); - } + if (writer.UseAbbreviatedSyntax && Position == null) + { + return true; } - private static IEnumerable? GetAliases(IEnumerable aliasAttributes, string argumentName) + if (writer.UseShortNamesForSyntax) { - if (!aliasAttributes.Any()) + if (HasLongName) { - return null; + return true; } + } + else if (HasShortName) + { + return true; + } - return aliasAttributes.Select(alias => - { - if (string.IsNullOrEmpty(alias.Alias)) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); - } + if (writer.IncludeAliasInDescription && (Aliases.Length > 0 || ShortAliases.Length > 0)) + { + return true; + } - return alias.Alias; - }); + if (writer.IncludeDefaultValueInDescription && IncludeDefaultInUsageHelp && DefaultValue != null) + { + return true; } - private static IEnumerable? GetShortAliases(IEnumerable aliasAttributes, string argumentName) + if (writer.IncludeValidatorsInDescription && + _validators.Any(v => !string.IsNullOrEmpty(v.GetUsageHelp(this)))) { - if (!aliasAttributes.Any()) - { - return null; - } + return true; + } - return aliasAttributes.Select(alias => - { - if (alias.Alias == '\0') + return false; + } + + internal CancelMode SetValue(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + { + _valueHelper ??= CreateValueHelper(); + + CancelMode cancelParsing; + if (MultiValueInfo?.Separator != null) + { + cancelParsing = CancelMode.None; + spanValue.Split(MultiValueInfo.Separator.AsSpan(), separateValue => + { + string? separateValueString = null; + PreValidate(ref separateValueString, separateValue); + var converted = ConvertToArgumentType(culture, true, separateValueString, separateValue); + cancelParsing = _valueHelper.SetValue(this, converted); + if (cancelParsing != CancelMode.Abort) { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); + Validate(converted, ValidationMode.AfterConversion); } - return alias.Alias; + return cancelParsing == CancelMode.None; }); } - - private static bool DetermineDictionaryValueTypeAllowsNull(Type type, PropertyInfo? property, ParameterInfo? parameter) + else { - var valueTypeNull = DetermineValueTypeNullable(type.GetGenericArguments()[1]); - if (valueTypeNull != null) - { - return valueTypeNull.Value; - } + PreValidate(ref stringValue, spanValue); + var converted = ConvertToArgumentType(culture, hasValue, stringValue, spanValue); + cancelParsing = _valueHelper.SetValue(this, converted); + Validate(converted, ValidationMode.AfterConversion); + } -#if NET6_0_OR_GREATER - // Type is the IDictionary<,> implemented interface, not the actual type of the property - // or parameter, which is what we need here. - var actualType = property?.PropertyType ?? parameter?.ParameterType; + HasValue = true; + return cancelParsing; + } - // We can only determine the nullability state if the property or parameter's actual - // type is Dictionary<,> or IDictionary<,>. Otherwise, we just assume nulls are - // allowed. - if (actualType != null && actualType.IsGenericType && - (actualType.GetGenericTypeDefinition() == typeof(Dictionary<,>) || actualType.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - var context = new NullabilityInfoContext(); - NullabilityInfo info; - if (property != null) - { - info = context.Create(property); - } - else - { - info = context.Create(parameter!); - } + internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } - return info.GenericTypeArguments[1].ReadState != NullabilityState.NotNull; - } -#endif + var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticHelpName(), parser.Options.ArgumentNameTransformOrDefault); + var shortName = parser.StringProvider.AutomaticHelpShortName(); + var shortAlias = char.ToLowerInvariant(argumentName[0]); + var existingArg = parser.GetArgument(argumentName) ?? + (parser.Mode == ParsingMode.LongShort + ? (parser.GetShortArgument(shortName) ?? parser.GetShortArgument(shortAlias)) + : (parser.GetArgument(shortName.ToString()) ?? parser.GetArgument(shortAlias.ToString()))); - return true; + if (existingArg != null) + { + return (existingArg, false); } - private static bool DetermineCollectionElementTypeAllowsNull(Type type, PropertyInfo? property, ParameterInfo? parameter) - { - Type elementType = type.IsArray ? type.GetElementType()! : type.GetGenericArguments()[0]; - var valueTypeNull = DetermineValueTypeNullable(elementType); - if (valueTypeNull != null) - { - return valueTypeNull.Value; - } + return (new HelpArgument(parser, argumentName, shortName, shortAlias), true); + } -#if NET6_0_OR_GREATER - // Type is the ICollection<> implemented interface, not the actual type of the property - // or parameter, which is what we need here. - var actualType = property?.PropertyType ?? parameter?.ParameterType; + internal static CommandLineArgument? CreateAutomaticVersion(CommandLineParser parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } - // We can only determine the nullability state if the property or parameter's actual - // type is an array or ICollection<>. Otherwise, we just assume nulls are allowed. - if (actualType != null && (actualType.IsArray || (actualType.IsGenericType && - actualType.GetGenericTypeDefinition() == typeof(ICollection<>)))) - { - var context = new NullabilityInfoContext(); - NullabilityInfo info; - if (property != null) - { - info = context.Create(property); - } - else - { - info = context.Create(parameter!); - } + var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticVersionName(), parser.Options.ArgumentNameTransformOrDefault); + if (parser.GetArgument(argumentName) != null) + { + return null; + } - if (actualType.IsArray) - { - return info.ElementType?.ReadState != NullabilityState.NotNull; - } - else - { - return info.GenericTypeArguments[0].ReadState != NullabilityState.NotNull; - } - } -#endif + return new VersionArgument(parser, argumentName); + } - return true; + internal void ApplyPropertyValue(object target) + { + // Do nothing for method-based values, or for required properties if the provider is not + // using reflection. + if (Kind == ArgumentKind.Method || (IsRequiredProperty && _parser.ProviderKind != ProviderKind.Reflection)) + { + return; } - private static bool DetermineAllowsNull(ParameterInfo parameter) + try { - var valueTypeNull = DetermineValueTypeNullable(parameter.ParameterType); - if (valueTypeNull != null) - { - return valueTypeNull.Value; - } - -#if NET6_0_OR_GREATER - var context = new NullabilityInfoContext(); - var info = context.Create(parameter); - return info.WriteState != NullabilityState.NotNull; -#else - return true; -#endif + _valueHelper?.ApplyValue(this, target); } - - private static bool DetermineAllowsNull(PropertyInfo property) + catch (TargetInvocationException ex) { - var valueTypeNull = DetermineValueTypeNullable(property.PropertyType); - if (valueTypeNull != null) - { - return valueTypeNull.Value; - } + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, this); + } + catch (Exception ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex, this); + } + } -#if NET6_0_OR_GREATER - var context = new NullabilityInfoContext(); - var info = context.Create(property); - return info.WriteState != NullabilityState.NotNull; -#else - return true; -#endif + internal void Reset() + { + if (MultiValueInfo == null && _defaultValue != null) + { + _valueHelper = new SingleValueHelper(_defaultValue); + } + else + { + _valueHelper = null; } - private static bool? DetermineValueTypeNullable(Type type) + HasValue = false; + _usedArgumentName = default; + } + + internal static void ShowVersion(LocalizedStringProvider stringProvider, Assembly assembly, string friendlyName) + { + Console.WriteLine(stringProvider.ApplicationNameAndVersion(assembly, friendlyName)); + var copyright = stringProvider.ApplicationCopyright(assembly); + if (copyright != null) { - if (type.IsValueType) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } + Console.WriteLine(copyright); + } + } - return null; + internal void ValidateAfterParsing() + { + if (HasValue) + { + Validate(null, ValidationMode.AfterParsing); + } + else if (IsRequired) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingRequiredArgument, ArgumentName); } + } + + internal void SetUsedArgumentName(ReadOnlyMemory name) + { + _usedArgumentName = name; + } - // Returns a tuple of (collectionType, dictionaryType, elementType) - private (Type?, Type?, Type?) DetermineMultiValueType() + private IValueHelper CreateValueHelper() + { + Debug.Assert(_valueHelper == null); + Type type; + switch (_argumentKind) { - // If the type is Dictionary it doesn't matter if the property is - // read-only or not. - if (_argumentType.IsGenericType && _argumentType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - { - var elementType = typeof(KeyValuePair<,>).MakeGenericType(_argumentType.GetGenericArguments()); - return (null, _argumentType, elementType); - } + case ArgumentKind.Dictionary: + type = typeof(DictionaryValueHelper<,>).MakeGenericType(_elementType.GetGenericArguments()); + return (IValueHelper)Activator.CreateInstance(type, DictionaryInfo!.AllowDuplicateKeys, _allowNull)!; - if (_argumentType.IsArray) - { - if (_argumentType.GetArrayRank() != 1) - { - throw new NotSupportedException(Properties.Resources.InvalidArrayRank); - } + case ArgumentKind.MultiValue: + type = typeof(MultiValueHelper<>).MakeGenericType(_elementTypeWithNullable); + return (IValueHelper)Activator.CreateInstance(type)!; - if (_property != null && _property.GetSetMethod() == null) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.PropertyIsReadOnlyFormat, _argumentName)); - } + case ArgumentKind.Method: + return new MethodValueHelper(); - var elementType = _argumentType.GetElementType()!; - return (_argumentType, null, elementType); - } + default: + Debug.Assert(_defaultValue == null); + return new SingleValueHelper(null); + } + } - // The interface approach requires a read-only property. If it's read-write, treat it - // like a non-multi-value argument. - // Don't use CanWrite because that returns true for properties with a private set - // accessor. - if (_property == null || _property.GetSetMethod() != null) - { - return (null, null, null); - } + internal static IEnumerable? GetAliases(IEnumerable? aliasAttributes, string argumentName) + { + if (aliasAttributes == null || !aliasAttributes.Any()) + { + return null; + } - var dictionaryType = TypeHelper.FindGenericInterface(_argumentType, typeof(IDictionary<,>)); - if (dictionaryType != null) + return aliasAttributes.Select(alias => + { + if (string.IsNullOrEmpty(alias.Alias)) { - var elementType = typeof(KeyValuePair<,>).MakeGenericType(dictionaryType.GetGenericArguments()); - return (null, dictionaryType, elementType); + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); } - var collectionType = TypeHelper.FindGenericInterface(_argumentType, typeof(ICollection<>)); - if (collectionType != null) - { - var elementType = collectionType.GetGenericArguments()[0]; - return (collectionType, null, elementType); - } + return alias.Alias; + }); + } - // This is a read-only property with an unsupported type. - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.PropertyIsReadOnlyFormat, _argumentName)); + internal static IEnumerable? GetShortAliases(IEnumerable? aliasAttributes, string argumentName) + { + if (aliasAttributes == null || !aliasAttributes.Any()) + { + return null; } - private static (MethodArgumentInfo, Type, bool)? DetermineMethodArgumentInfo(MethodInfo method) + return aliasAttributes.Select(alias => { - var parameters = method.GetParameters(); - if (!method.IsStatic || - (method.ReturnType != typeof(bool) && method.ReturnType != typeof(void)) || - parameters.Length > 2) + if (alias.Alias == '\0') { - return null; + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); } - bool allowsNull = false; - var argumentType = typeof(bool); - var info = new MethodArgumentInfo() { Method = method }; - if (parameters.Length == 2) - { - argumentType = parameters[0].ParameterType; - if (parameters[1].ParameterType != typeof(CommandLineParser)) - { - return null; - } - - info.HasValueParameter = true; - info.HasParserParameter = true; - } - else if (parameters.Length == 1) - { - if (parameters[0].ParameterType == typeof(CommandLineParser)) - { - info.HasParserParameter = true; - } - else - { - argumentType = parameters[0].ParameterType; - info.HasValueParameter = true; - } - } + return alias.Alias; + }); + } - if (info.HasValueParameter) - { - allowsNull = DetermineAllowsNull(parameters[0]); - } + private static CancelMode AutomaticVersion(CommandLineParser parser) + { + ShowVersion(parser.StringProvider, parser.ArgumentsType.Assembly, parser.ApplicationFriendlyName); - return (info, argumentType, allowsNull); - } + // Cancel parsing but do not show help. + return CancelMode.Abort; + } - private static void AutomaticHelp() + internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform transform) + { + if (explicitName != null) { - // Intentionally blank. + return explicitName; } - private static bool AutomaticVersion(CommandLineParser parser) - { - ShowVersion(parser.StringProvider, parser.ArgumentsType.Assembly, parser.ApplicationFriendlyName); + return transform.Apply(memberName); + } - // Cancel parsing but do not show help. - return false; + private string? GetDefaultValueDescription(Type? type) + { + if (Parser.Options.DefaultValueDescriptions == null || + !Parser.Options.DefaultValueDescriptions.TryGetValue(type ?? ElementType, out string? value)) + { + return null; } - private static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) + return value; + } + + private void Validate(object? value, ValidationMode mode) + { + foreach (var validator in _validators) { - if (explicitName != null) + if (validator.Mode == mode) { - return explicitName; + validator.Validate(this, value); } - - return transform?.Apply(memberName) ?? memberName; } + } - private void Validate(object? value, ValidationMode mode) + private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) + { + foreach (var validator in _validators) { - foreach (var validator in _validators) + if (validator.Mode == ValidationMode.BeforeConversion) { - if (validator.Mode == mode) + if (stringValue == null) { - validator.Validate(this, value); + if (validator.ValidateSpan(this, spanValue)) + { + continue; + } + else + { + stringValue = spanValue.ToString(); + } } - } - } - - private static string? GetDefaultValueDescription(Type type, IDictionary? defaultValueDescriptions) - { - if (defaultValueDescriptions == null) - { - return null; - } - if (defaultValueDescriptions.TryGetValue(type, out string? value)) - { - return value; + validator.Validate(this, stringValue); } - - return null; } + } - private static string DetermineValueDescription(Type type, ParseOptions options) - { - var result = GetDefaultValueDescription(type, options.DefaultValueDescriptions); - if (result == null) - { - var typeName = GetFriendlyTypeName(type); - result = options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; - } - - return result; - } + internal static MultiValueArgumentInfo GetMultiValueInfo(MultiValueSeparatorAttribute? attribute) + { + var separator = attribute?.Separator; + return new( + string.IsNullOrEmpty(separator) ? null : separator, + attribute != null && separator == null + ); } } diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 2f8d359e..e5e9e808 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -1,343 +1,375 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Commands; using System; -using System.ComponentModel; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates a property or method of a class defines a command line argument. +/// +/// +/// +/// If this attribute is applied to a property, the property's type determines the argument +/// type, and the property will be set with either the set value or the default value after +/// parsing is complete. +/// +/// +/// If an argument was not provided on the command line, and the default value is , +/// the property will not be set and will remain at its initial value. +/// +/// +/// If this attribute is applied to a method, that method must have one of the following +/// signatures: +/// +/// +/// public static (void|bool|CancelMode) Method(ArgumentType value, CommandLineParser parser); +/// public static (void|bool|CancelMode) Method(ArgumentType value); +/// public static (void|bool|CancelMode) Method(CommandLineParser parser); +/// public static (void|bool|CancelMode) Method(); +/// +/// +/// In this case, the ArgumentType type determines the type of values the argument accepts. If there +/// is no value parameter, the argument will be a switch argument, and the method will +/// be invoked if the switch is present, even if it was explicitly set to . +/// +/// +/// The method will be invoked as soon as the argument is parsed, before parsing the entire +/// command line is complete. +/// +/// +/// The return type must be either , or . +/// Using is equivalent to returning , and when +/// using , returning is equivalent to returning +/// . +/// +/// +/// Unlike using the property, canceling parsing with the return +/// value does not automatically print the usage help when using the +/// method, the +/// method or the +/// class. Instead, it must be requested using by setting the +/// property to in the +/// target method. +/// +/// +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +public sealed class CommandLineArgumentAttribute : Attribute { + private readonly string? _argumentName; + private bool _short; + private bool _isPositional; + + /// + /// Initializes a new instance of the class using the specified argument name. + /// + /// + /// The name of the argument, or to indicate the member name + /// should be used, applying the specified by the + /// property or the + /// property. + /// + /// + /// If the property is , + /// the parameter is the long name of the argument. + /// + /// + /// If the property is + /// and the property is , the + /// parameter will not be used. + /// + /// + /// + /// The will not be applied to explicitly specified names. + /// + /// + public CommandLineArgumentAttribute(string? argumentName = null) + { + _argumentName = argumentName; + } + /// - /// Indicates a property or method of a class defines a command line argument. + /// Gets the name of the argument. /// + /// + /// The name that can be used to supply the argument, or if the + /// member name should be used. + /// /// /// - /// If this attribute is applied to a property, the property's type determines the argument - /// type, and the property will be set with either the set value or the default value after - /// parsing is complete. + /// If the property is , + /// this is the long name of the argument. /// /// - /// If an argument was not provided on the command line, and the default value is , - /// the property will not be set and will remain at its initial value. + /// If the property is + /// and the property is , the + /// property is ignored. /// + /// + /// + public string? ArgumentName + { + get { return _argumentName; } + } + + /// + /// Gets or sets a value that indicates whether the argument has a long name. + /// + /// + /// if the argument has a long name; otherwise, . + /// The default value is . + /// + /// /// - /// If this attribute is applied to a method, that method must have one of the following - /// signatures: + /// This property is ignored if is not + /// . /// - /// - /// public static bool Method(ArgumentType value, CommandLineParser parser); - /// public static bool Method(ArgumentType value); - /// public static bool Method(CommandLineParser parser); - /// public static bool Method(); - /// public static void Method(ArgumentType value, CommandLineParser parser); - /// public static void Method(ArgumentType value); - /// public static void Method(CommandLineParser parser); - /// public static void Method(); - /// /// - /// In this case, the ArgumentType type determines the type of values the argument accepts. If there - /// is no value parameter, the argument will be a switch argument, and the method will - /// be invoked if the switch is present, even if it was explicitly set to . + /// If the property is + /// and the property is , the + /// property is ignored. /// + /// + /// + public bool IsLong { get; set; } = true; + + /// + /// Gets or sets a value that indicates whether the argument has a short name. + /// + /// + /// if the argument has a short name; otherwise, . + /// The default value is . + /// + /// + /// + /// This property is ignored if the + /// property is not . + /// /// - /// The method will be invoked as soon as the argument is parsed, before parsing the entire - /// command line is complete. Return to cancel parsing, in which case - /// the remaining arguments will not be parsed and the - /// method returns . + /// If the property is not set but this property is set to , + /// the short name will be derived using the first character of the long name. /// /// - /// Unlike using the or - /// event, canceling parsing with the return value does not automatically print the usage - /// help when using the method, the - /// method or the - /// class. Instead, it must be requested using by setting the - /// property to . + /// If the property is set to a value other than the null character, + /// this property will always return . /// /// - /// - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - public sealed class CommandLineArgumentAttribute : Attribute + /// + public bool IsShort { - private readonly string? _argumentName; - private bool _short; - - /// - /// Initializes a new instance of the class using the specified argument name. - /// - /// - /// The name of the argument, or to indicate the member name - /// should be used, applying the specified by the - /// property or the - /// property. - /// - /// - /// If the property is , - /// the parameter is the long name of the argument. - /// - /// - /// If the property is - /// and the property is , the - /// parameter will not be used. - /// - /// - /// - /// The will not be applied to explicitly specified names. - /// - /// - public CommandLineArgumentAttribute(string? argumentName = null) - { - _argumentName = argumentName; - } - - /// - /// Gets the name of the argument. - /// - /// - /// The name that can be used to supply the argument, or if the - /// member name should be used. - /// - /// - /// - /// If the property is , - /// this is the long name of the argument. - /// - /// - /// If the property is - /// and the property is , the - /// property is ignored. - /// - /// - /// - public string? ArgumentName - { - get { return _argumentName; } - } - - /// - /// Gets or sets a value that indicates whether the argument has a long name. - /// - /// - /// if the argument has a long name; otherwise, . - /// The default value is . - /// - /// - /// - /// This property is ignored if is not - /// . - /// - /// - /// If the property is - /// and the property is , the - /// property is ignored. - /// - /// - /// - public bool IsLong { get; set; } = true; + get => _short || ShortName != '\0'; + set => _short = value; + } - /// - /// Gets or sets a value that indicates whether the argument has a short name. - /// - /// - /// if the argument has a short name; otherwise, . - /// The default value is . - /// - /// - /// - /// This property is ignored if is not - /// . - /// - /// - /// If the property is not set but this property is set to , - /// the short name will be derived using the first character of the long name. - /// - /// - /// - public bool IsShort - { - get => _short || ShortName != '\0'; - set => _short = value; - } + /// + /// Gets or sets the argument's short name. + /// + /// The short name, or a null character ('\0') if the argument has no short name. + /// + /// + /// This property is ignored if the + /// property is not . + /// + /// + /// Setting this property implies the property is . + /// + /// + /// To derive the short name from the first character of the long name, set the + /// property to without setting the + /// property. + /// + /// + /// + public char ShortName { get; set; } - /// - /// Gets or sets the argument's short name. - /// - /// The short name, or a null character ('\0') if the argument has no short name. - /// - /// - /// This property is ignored if is not - /// . - /// - /// - /// Setting this property implies the property is . - /// - /// - /// To derive the short name from the first character of the long name, set the - /// property to without setting the - /// property. - /// - /// - /// - public char ShortName { get; set; } + /// + /// Gets or sets a value indicating whether the argument is required. + /// + /// + /// if the argument must be supplied on the command line; otherwise, . + /// The default value is . + /// + /// + /// + /// If the attribute is used on a property with + /// the C# required keyword, the argument will always be required, and the value of + /// this property is ignored. + /// + /// + /// + public bool IsRequired { get; set; } - /// - /// Gets or sets a value indicating whether the argument is required. - /// - /// - /// if the argument must be supplied on the command line; otherwise, . - /// The default value is . - /// - /// - public bool IsRequired { get; set; } + /// + /// Gets or sets the relative position of a positional argument. + /// + /// + /// The position of the argument, or a negative value if the argument can only be specified by name. The default value is -1. + /// + /// + /// + /// The property specifies the relative position of the positional + /// arguments created by properties. The actual numbers are not important, only their + /// order is. For example, if you have two positional arguments with positions set to + /// 4 and 7, and no other positional arguments, they will be the first and second + /// positional arguments, not the forth and seventh. It is an error to use the same number + /// more than once. + /// + /// + /// When using the , you can also set the + /// property to , without setting the property, + /// to order the positional arguments using the order of the members that define them. + /// + /// + /// If you set the property to a non-negative value, it is not + /// necessary to set the property. + /// + /// + /// The property will be set to reflect the actual position of the argument, + /// which may not match the value of this property. + /// + /// + /// + public int Position { get; set; } = -1; - /// - /// Gets or sets the position of a positional argument. - /// - /// - /// The position of the argument, or a negative value if the argument can only be specified by name. The default value is -1. - /// - /// - /// - /// The property specifies the relative position of the positional - /// arguments created by properties. The actual numbers are not important, only their - /// order is. For example, if you have two positional arguments with positions set to - /// 4 and 7, and no other positional arguments, they will be the first and second - /// positional arguments, not the forth and seventh. It is an error to use the same number - /// more than once. - /// - /// - /// If you have arguments defined by the type's constructor parameters, positional arguments defined by properties will - /// always come after them; for example, if you have two constructor parameter arguments and one property positional argument with - /// position 0, then that argument will actually be the third positional argument. - /// - /// - /// The property will be set to reflect the actual position of the argument, - /// which may not match the value of the property. - /// - /// - /// - public int Position { get; set; } = -1; + /// + /// Gets or sets a value that indicates that an argument is positional. + /// + /// + /// if the argument is positional; otherwise, . + /// + /// + /// + /// If the property is set to a non-negative value, this property + /// always returns . + /// + /// + /// When using the attribute, you can set the + /// property to without setting the + /// property, to order positional arguments using the order of the + /// members that define them. + /// + /// + /// Doing this is not supported without the attribute, + /// because reflection is not guaranteed to return class members in any particular order. The + /// class will throw an exception if the + /// property is without a non-negative property + /// value if reflection is used. + /// + /// + public bool IsPositional + { + get => _isPositional || Position >= 0; + set => _isPositional = value; + } - /// - /// Gets or sets the default value to be assigned to the property if the argument is not supplied on the command line. - /// - /// - /// The default value for the argument, or to not set the property - /// if the argument is not supplied. The default value is . - /// - /// - /// - /// The property will not be used if the property is , - /// or if the argument is a multi-value or dictionary argument, or if the - /// attribute was applied to a method. - /// - /// - /// By default, the command line usage help generated by - /// does not include the default value. Either manually add it to the description, or set the - /// property to . - /// - /// - /// - public object? DefaultValue { get; set; } + /// + /// Gets or sets the default value to be assigned to the property if the argument is not supplied on the command line. + /// + /// + /// The default value for the argument, or to not set the property + /// if the argument is not supplied. The default value is . + /// + /// + /// + /// The property will not be used if the property is , + /// or if the argument is a multi-value or dictionary argument, or if the + /// attribute was applied to a method. + /// + /// + /// By default, the command line usage help generated by + /// includes the default value. To change that, set the + /// property to , or to change it for all arguments set the + /// property to + /// . + /// + /// + /// The default value can also be set by using a property initializer. When using the + /// attribute, a default value set using a property + /// initializer will also be shown in the usage help, as long as it's a literal, enumeration + /// value, or constant. Without the attribute, only default values set with the + /// property are shown in the usage help. + /// + /// + /// + public object? DefaultValue { get; set; } - /// - /// Gets or sets a short description of the property's value to use when printing usage information. - /// - /// - /// The description of the value, or to indicate that the property's - /// type name should be used, applying the specified by the - /// or - /// property. - /// - /// - /// - /// The value description is a short, typically one-word description that indicates the - /// type of value that the user should supply. - /// - /// - /// If not specified here, it is retrieved from the - /// property, and if not found there, the type of the property is used, applying the - /// specified by the - /// property or the property. - /// If this is a multi-value argument, the element type is used. If the type is , - /// its underlying type is used. - /// - /// - /// If you want to override the value description for all arguments of a specific type, - /// use the property. - /// - /// - /// The value description is used only when generating usage help. For example, the usage for an argument named Sample with - /// a value description of String would look like "-Sample <String>". - /// - /// - /// This is not the long description used to describe the purpose of the argument. That can be set - /// using the attribute. - /// - /// - /// - public string? ValueDescription { get; set; } + /// + /// Gets or sets a value that indicates whether the argument's default value should be shown + /// in the usage help. + /// + /// + /// to show the default value in the usage help; otherwise, + /// . The default value is . + /// + /// + /// + /// The default value can be set using the property, or, when + /// using source generation with the attribute, using a + /// property initializer. + /// + /// + /// This property is ignored if the + /// property is . + /// + /// + public bool IncludeDefaultInUsageHelp { get; set; } = true; - /// - /// Gets or sets a value that indicates whether argument parsing should be canceled if - /// this argument is encountered. - /// - /// - /// if argument parsing should be canceled after this argument; - /// otherwise, . The default value is . - /// - /// - /// - /// If this property is , the will - /// stop parsing the command line arguments after seeing this argument, and return - /// from the method - /// or one of its overloads. Since no instance of the arguments type is returned, it's - /// not possible to determine argument values, or which argument caused the cancellation, - /// except by inspecting the property. - /// - /// - /// This property is most commonly useful to implement a "-Help" or "-?" style switch - /// argument, where the presence of that argument causes usage help to be printed and - /// the program to exit, regardless of whether the rest of the command line is valid - /// or not. - /// - /// - /// The method and the - /// static helper method - /// will print usage information if parsing was canceled through this method. - /// - /// - /// Canceling parsing in this way is identical to handling the - /// event and setting to - /// . - /// - /// - /// It's possible to prevent cancellation when an argument has this property set by - /// handling the event and setting the - /// property to - /// . - /// - /// - /// - public bool CancelParsing { get; set; } + /// + /// Gets or sets a value that indicates whether argument parsing should be canceled if + /// this argument is encountered. + /// + /// + /// One of the values of the enumeration. + /// + /// + /// + /// If this property is not , the + /// will stop parsing the command line arguments after seeing this argument. The result of + /// the operation will be if this property is , + /// or an instance of the arguments class with the results up to this point if this property + /// is . In the latter case, the + /// property will contain all arguments that were not parsed. + /// + /// + /// If is used, all required arguments must have a value at + /// the point this argument is encountered, otherwise a + /// is thrown. + /// + /// + /// Use the property to determine which argument caused + /// cancellation. + /// + /// + /// If this property is , the + /// property will be automatically set to when parsing is canceled. + /// + /// + /// It's possible to prevent cancellation when an argument has this property set by + /// handling the event and setting the + /// property to . + /// + /// + /// + public CancelMode CancelParsing { get; set; } - /// - /// Gets or sets a value that indicates whether the argument is hidden from the usage help. - /// - /// - /// if the argument is hidden from the usage help; otherwise, - /// . The default value is . - /// - /// - /// - /// A hidden argument will not be included in the usage syntax or the argument description - /// list, even if is used. - /// - /// - /// This property is ignored for positional or required arguments, which may not be - /// hidden. - /// - /// - /// - public bool IsHidden { get; set; } - } + /// + /// Gets or sets a value that indicates whether the argument is hidden from the usage help. + /// + /// + /// if the argument is hidden from the usage help; otherwise, + /// . The default value is . + /// + /// + /// + /// A hidden argument will not be included in the usage syntax or the argument description + /// list, even if is used. + /// + /// + /// This property is ignored for positional or required arguments, which may not be + /// hidden. + /// + /// + /// + public bool IsHidden { get; set; } } diff --git a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs index 704a0a5d..08486313 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs @@ -1,71 +1,68 @@ -// Copyright (c) Sven Groot (Ookii.org) + +namespace Ookii.CommandLine; -namespace Ookii.CommandLine +/// +/// Specifies the kind of error that occurred while parsing arguments. +/// +public enum CommandLineArgumentErrorCategory { /// - /// Specifies the kind of error that occurred while parsing arguments. + /// The category was not specified. /// - public enum CommandLineArgumentErrorCategory - { - /// - /// The category was not specified. - /// - Unspecified, - /// - /// The argument value supplied could not be converted to the type of the argument. - /// - ArgumentValueConversion, - /// - /// The argument name supplied does not name a known argument. - /// - UnknownArgument, - /// - /// An argument name was supplied, but without an accompanying value. - /// - MissingNamedArgumentValue, - /// - /// An argument was supplied more than once. - /// - DuplicateArgument, - /// - /// Too many positional arguments were supplied. - /// - TooManyArguments, - /// - /// Not all required arguments were supplied. - /// - MissingRequiredArgument, - /// - /// Invalid value for a dictionary argument; typically the result of a duplicate key or - /// a value without a key/value separator. - /// - InvalidDictionaryValue, - /// - /// An error occurred creating an instance of the arguments type (e.g. the constructor threw an exception). - /// - CreateArgumentsTypeError, - /// - /// An error occurred applying the value of the argument (e.g. the property set accessor threw an exception). - /// - ApplyValueError, - /// - /// An argument value was after conversion from a string, and the argument type is a value - /// type or (in .Net 6.0 and later) a non-nullable reference type. - /// - NullArgumentValue, - /// - /// A combined short argument contains an argument that is not a switch. - /// - CombinedShortNameNonSwitch, - /// - /// An instance of a class derived from the - /// class failed to validate the argument. - /// - ValidationFailed, - /// - /// An argument failed a dependency check performed by the - /// or the class. - /// - DependencyFailed, - } + Unspecified, + /// + /// The argument value supplied could not be converted to the type of the argument. + /// + ArgumentValueConversion, + /// + /// The argument name supplied does not name a known argument. + /// + UnknownArgument, + /// + /// An argument name was supplied, but without an accompanying value. + /// + MissingNamedArgumentValue, + /// + /// An argument was supplied more than once. + /// + DuplicateArgument, + /// + /// Too many positional arguments were supplied. + /// + TooManyArguments, + /// + /// Not all required arguments were supplied. + /// + MissingRequiredArgument, + /// + /// Invalid value for a dictionary argument; typically the result of a duplicate key. + /// + InvalidDictionaryValue, + /// + /// An error occurred creating an instance of the arguments type (e.g. the constructor threw an exception). + /// + CreateArgumentsTypeError, + /// + /// An error occurred applying the value of the argument (e.g. the property set accessor threw an exception). + /// + ApplyValueError, + /// + /// An argument value was after conversion from a string, and the argument type is a value + /// type or (in .Net 6.0 and later) a non-nullable reference type. + /// + NullArgumentValue, + /// + /// A combined short argument contains an argument that is not a switch. + /// + CombinedShortNameNonSwitch, + /// + /// An instance of a class derived from the + /// class failed to validate the argument. + /// + ValidationFailed, + /// + /// An argument failed a dependency check performed by the + /// or the class. + /// + DependencyFailed, } diff --git a/src/Ookii.CommandLine/CommandLineArgumentException.cs b/src/Ookii.CommandLine/CommandLineArgumentException.cs index ed31ade2..1713f348 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentException.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentException.cs @@ -1,167 +1,170 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; using System.Security.Permissions; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// The exception that is thrown when command line parsing failed due to an invalid command line. +/// +/// +/// +/// This exception indicates that the command line passed to the +/// method, or +/// another parsing method, was invalid for the arguments defined by the +/// instance. +/// +/// +/// Use the property to determine the exact cause of the exception. +/// +/// +/// +[Serializable] +public class CommandLineArgumentException : Exception { + private readonly string? _argumentName; + private readonly CommandLineArgumentErrorCategory _category; + /// - /// The exception that is thrown when command line parsing failed due to an invalid command line. + /// Initializes a new instance of the class. /// - /// - /// - /// This exception indicates that the command line passed to the method - /// was invalid for the arguments defined by the instance. - /// - /// - /// The exception can indicate that too many positional arguments were supplied, a required argument was not supplied, an unknown argument name was supplied, - /// no value was supplied for a named argument, an argument was supplied more than once and the property - /// is , or one of the argument values could not be converted to the argument's type. - /// - /// - /// - [Serializable] - public class CommandLineArgumentException : Exception - { - private readonly string? _argumentName; - private readonly CommandLineArgumentErrorCategory _category; - - /// - /// Initializes a new instance of the class. - /// - public CommandLineArgumentException() { } + public CommandLineArgumentException() { } - /// - /// - /// Initializes a new instance of the class with a specified error message. - /// - public CommandLineArgumentException(string? message) : base(message) { } + /// + /// + /// Initializes a new instance of the class with a specified error message. + /// + public CommandLineArgumentException(string? message) : base(message) { } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - /// The category of this error. - public CommandLineArgumentException(string? message, CommandLineArgumentErrorCategory category) - : base(message) - { - _category = category; - } + /// + /// Initializes a new instance of the class with a + /// specified error message and category. + /// + /// The message that describes the error. + /// The category of this error. + public CommandLineArgumentException(string? message, CommandLineArgumentErrorCategory category) + : base(message) + { + _category = category; + } - /// - /// Initializes a new instance of the class with - /// a specified error message, argument name and category. - /// - /// The message that describes the error. - /// The name of the argument that was invalid. - /// The category of this error. - public CommandLineArgumentException(string? message, string? argumentName, CommandLineArgumentErrorCategory category) - : base(message) - { - _argumentName = argumentName; - _category = category; - } + /// + /// Initializes a new instance of the class with + /// a specified error message, argument name and category. + /// + /// The message that describes the error. + /// The name of the argument that was invalid. + /// The category of this error. + public CommandLineArgumentException(string? message, string? argumentName, CommandLineArgumentErrorCategory category) + : base(message) + { + _argumentName = argumentName; + _category = category; + } - /// - /// Initializes a new instance of the class with - /// a specified error message and a reference to the inner that is - /// the cause of this . - /// - /// The message that describes the error. - /// - /// The that is the cause of the current , - /// or if no inner is specified. - /// - public CommandLineArgumentException(string? message, Exception? inner) : base(message, inner) { } + /// + /// Initializes a new instance of the class with + /// a specified error message and a reference to the inner that is + /// the cause of this . + /// + /// The message that describes the error. + /// + /// The that is the cause of the current , + /// or if no inner is specified. + /// + public CommandLineArgumentException(string? message, Exception? inner) : base(message, inner) { } - /// - /// Initializes a new instance of the class with - /// a specified error message, category, and a reference to the inner that is - /// the cause of this . - /// - /// The error message that explains the reason for the . - /// The category of this error. - /// - /// The that is the cause of the current , - /// or a if no inner is specified. - /// - public CommandLineArgumentException(string? message, CommandLineArgumentErrorCategory category, Exception? inner) - : base(message, inner) - { - _category = category; - } + /// + /// Initializes a new instance of the class with + /// a specified error message, category, and a reference to the inner that is + /// the cause of this . + /// + /// The error message that explains the reason for the . + /// The category of this error. + /// + /// The that is the cause of the current , + /// or a if no inner is specified. + /// + public CommandLineArgumentException(string? message, CommandLineArgumentErrorCategory category, Exception? inner) + : base(message, inner) + { + _category = category; + } - /// - /// Initializes a new instance of the class with - /// a specified error message, argument name, category, and a reference to the inner - /// that is the cause of this . - /// - /// The error message that explains the reason for the . - /// The name of the argument that was invalid. - /// The category of this error. - /// - /// The that is the cause of the current , - /// or a if no inner is specified. - /// - public CommandLineArgumentException(string? message, string? argumentName, CommandLineArgumentErrorCategory category, Exception? inner) - : base(message, inner) - { - _argumentName = argumentName; - _category = category; - } + /// + /// Initializes a new instance of the class with + /// a specified error message, argument name, category, and a reference to the inner + /// that is the cause of this . + /// + /// The error message that explains the reason for the . + /// + /// The name of the argument that was invalid, or if the error was not + /// caused by a particular argument. + /// + /// The category of this error. + /// + /// The that is the cause of the current , + /// or a if no inner is specified. + /// + public CommandLineArgumentException(string? message, string? argumentName, CommandLineArgumentErrorCategory category, Exception? inner) + : base(message, inner) + { + _argumentName = argumentName; + _category = category; + } - /// - /// - /// Initializes a new instance of the class with serialized data. - /// - protected CommandLineArgumentException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) - : base(info, context) - { - _argumentName = info.GetString("ArgumentName"); - _category = (CommandLineArgumentErrorCategory?)info.GetValue("Category", typeof(CommandLineArgumentErrorCategory)) ?? CommandLineArgumentErrorCategory.Unspecified; - } + /// + /// + /// Initializes a new instance of the class with serialized data. + /// + protected CommandLineArgumentException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + _argumentName = info.GetString("ArgumentName"); + _category = (CommandLineArgumentErrorCategory?)info.GetValue("Category", typeof(CommandLineArgumentErrorCategory)) ?? CommandLineArgumentErrorCategory.Unspecified; + } - /// - /// Gets the name of the argument that was invalid. - /// - /// - /// The name of the invalid argument, or if the error does not refer to a specific argument. - /// - public string? ArgumentName - { - get { return _argumentName; } - } + /// + /// Gets the name of the argument that was invalid. + /// + /// + /// The name of the invalid argument, or if the error does not refer to a specific argument. + /// + public string? ArgumentName + { + get { return _argumentName; } + } - /// - /// Gets the category of this error. - /// - /// - /// One of the values of the enumeration indicating the kind of error that occurred. - /// - public CommandLineArgumentErrorCategory Category - { - get { return _category; } - } + /// + /// Gets the category of this error. + /// + /// + /// One of the values of the enumeration indicating the kind of error that occurred. + /// + public CommandLineArgumentErrorCategory Category + { + get { return _category; } + } - /// - /// Sets the object with the parameter name and additional exception information. - /// - /// The object that holds the serialized object data. - /// The contextual information about the source or destination. - /// is . + /// + /// Sets the object with the + /// argument name and additional exception information. + /// + /// The object that holds the serialized object data. + /// The contextual information about the source or destination. + /// is . #if !NET6_0_OR_GREATER - [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] #endif - public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + { + if (info == null) { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } + throw new ArgumentNullException(nameof(info)); + } - base.GetObjectData(info, context); + base.GetObjectData(info, context); - info.AddValue("ArgumentName", ArgumentName); - info.AddValue("Category", Category); - } + info.AddValue("ArgumentName", ArgumentName); + info.AddValue("Category", Category); } } diff --git a/src/Ookii.CommandLine/CommandLineConstructorAttribute.cs b/src/Ookii.CommandLine/CommandLineConstructorAttribute.cs deleted file mode 100644 index 0b64bd65..00000000 --- a/src/Ookii.CommandLine/CommandLineConstructorAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; - -namespace Ookii.CommandLine -{ - /// - /// Indicates the constructor that should be used by the class, if a class has multiple public constructors. - /// - /// - /// - /// If a class has only one public constructor, it is not necessary to use this attribute. - /// - /// - /// - [AttributeUsage(AttributeTargets.Constructor)] - public sealed class CommandLineConstructorAttribute : Attribute - { - } -} diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index dc7f8fc8..8cceac9f 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1,1650 +1,1809 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Support; +using Ookii.CommandLine.Terminal; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.Immutable; using System.ComponentModel; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Parses command line arguments defined by a type's properties and methods. +/// +/// +/// +/// The class parses command line arguments into named, +/// strongly-typed values. The accepted arguments are defined by the properties and methods of the +/// type passed to the constructor. The result +/// of a parsing operation is an instance of that type, created using the values that were +/// supplied on the command line. +/// +/// +/// The arguments type must have a constructor that has no parameters, or a single parameter +/// with the type , which will receive the instance of the +/// class that was used to parse the arguments. +/// +/// +/// A property defines a command line argument if it is , not +/// , and has the attribute +/// applied. The attribute has properties to +/// determine the behavior of the argument, such as whether it's required or positional. +/// +/// +/// A method defines a command line argument if it is , , +/// has the attribute applied, and one of the +/// signatures shown in the documentation for the +/// attribute. +/// +/// +/// To parse arguments, invoke the method or one of its overloads, or use +/// or one of its overloads to automatically handle +/// errors and print usage help when requested. +/// +/// +/// The static method is a helper that create a +/// instance, and parse arguments with error handling in a single +/// call. If using source generation with the attribute, +/// you can also use the generated +/// method. +/// +/// +/// The derived type provides strongly-typed instance and +/// methods, if you don't wish to use the static methods. +/// +/// +/// The class is for applications with a single (root) command. +/// If you wish to create an application with subcommands, use the +/// class instead. +/// +/// +/// The supports two sets of rules for how to parse arguments; +/// mode and mode. For +/// more details on these rules, please see +/// the documentation on GitHub. +/// +/// +/// +/// +/// +/// Usage documentation +public class CommandLineParser { + #region Nested types + + private sealed class CommandLineArgumentComparer : IComparer + { + private readonly StringComparison _comparison; + + public CommandLineArgumentComparer(StringComparison comparison) + { + _comparison = comparison; + } + + public int Compare(CommandLineArgument? x, CommandLineArgument? y) + { + if (x == null) + { + if (y == null) + { + return 0; + } + else + { + return -1; + } + } + else if (y == null) + { + return 1; + } + + // Positional arguments come before non-positional ones, and must be sorted by position + if (x.Position != null) + { + if (y.Position != null) + { + return x.Position.Value.CompareTo(y.Position.Value); + } + else + { + return -1; + } + } + else if (y.Position != null) + { + return 1; + } + + // Non-positional required arguments come before optional arguments + if (x.IsRequired) + { + if (!y.IsRequired) + { + return -1; + } + // If both are required, sort by name + } + else if (y.IsRequired) + { + return 1; + } + + // Sort the rest by name + return string.Compare(x.ArgumentName, y.ArgumentName, _comparison); + } + } + + private sealed class MemoryComparer : IComparer> + { + private readonly StringComparison _comparison; + + public MemoryComparer(StringComparison comparison) + { + _comparison = comparison; + } + + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) => x.Span.CompareTo(y.Span, _comparison); + } + + private sealed class CharComparer : IComparer + { + private readonly StringComparison _comparison; + + public CharComparer(StringComparison comparison) + { + _comparison = comparison; + } + + public int Compare(char x, char y) + { + unsafe + { + // If anyone knows a better way to compare individual chars according to a + // StringComparison, I'd be happy to hear it. + var spanX = new ReadOnlySpan(&x, 1); + var spanY = new ReadOnlySpan(&y, 1); + return spanX.CompareTo(spanY, _comparison); + } + } + } + + private struct PrefixInfo + { + public string Prefix { get; set; } + public bool Short { get; set; } + } + + #endregion + + private readonly ArgumentProvider _provider; + private readonly ImmutableArray _arguments; + private readonly SortedDictionary, CommandLineArgument> _argumentsByName; + private readonly SortedDictionary? _argumentsByShortName; + private readonly int _positionalArgumentCount; + + private readonly ParseOptions _parseOptions; + private readonly ParsingMode _mode; + private readonly PrefixInfo[] _sortedPrefixes; + private readonly ImmutableArray _argumentNamePrefixes; + private readonly string? _longArgumentNamePrefix; + private readonly ImmutableArray _nameValueSeparators; + + private List? _requiredPropertyArguments; + /// - /// Parses command line arguments defined by a class of the specified type. + /// Gets the default prefix used for long argument names if the + /// property is . /// + /// + /// The default long argument name prefix, which is '--'. + /// /// /// - /// The class can parse a set of command line arguments into - /// values. Which arguments are accepted is determined from the constructor parameters, - /// properties, and methods of the type passed to the - /// constructor. The result of a parsing operation is an instance of that type, created using - /// the values that were supplied on the command line. + /// This constant is used as the default value of the + /// property if no custom value was specified using the + /// property of the + /// property. /// + /// + public const string DefaultLongArgumentNamePrefix = "--"; + + /// + /// Event raised when an argument is parsed from the command line. + /// + /// /// - /// An argument defined by a constructor parameter is always positional, and is required if - /// the parameter has no default value. If your type has multiple constructors, use the - /// attribute to indicate which one to use. + /// Set the property in + /// the event handler to cancel parsing at the current argument. To have usage help shown + /// by the parse methods that do this automatically, you must set the + /// property to explicitly in the event handler. /// /// - /// A constructor parameter with the type is not an argument, - /// but will be passed the instance of the class used to - /// parse the arguments when the type is instantiated. + /// The property is + /// initialized to the value of the + /// property, or the method return value of an argument using . + /// Reset the value to to continue parsing + /// anyway. /// /// - /// A property defines a command line argument if it is , not - /// , and has the attribute - /// defined. The properties of the argument are determined by the properties of the - /// class. + /// This event is invoked after the + /// and properties have + /// been set. /// + /// + public event EventHandler? ArgumentParsed; + + /// + /// Event raised when an argument that is not multi-value is specified more than once. + /// + /// /// - /// A method defines a command line argument if it is , , - /// has the attribute applied, and one of the - /// signatures shown in the documentation for the - /// attribute. + /// Handling this event allows you to inspect the new value, and decide to keep the old + /// or new value. It also allows you to, for instance, print a warning for duplicate + /// arguments. /// /// - /// To parse arguments, invoke the method or one of its overloads. - /// The static method is a helper that will - /// parse arguments and print error and usage information if required. Calling this method - /// will be sufficient for most use cases. + /// This even is only raised when the property is + /// . /// + /// + public event EventHandler? DuplicateArgument; + + internal const string UnreferencedCodeHelpUrl = "https://www.ookii.org/Link/CommandLineSourceGeneration"; + + /// + /// Initializes a new instance of the class using the + /// specified arguments type and options. + /// + /// The of the class that defines the command line arguments. + /// + /// The options that control parsing behavior, or to use the + /// default options. + /// + /// + /// is . + /// + /// + /// The cannot use as the command line arguments type, + /// because it violates one of the rules concerning argument names or positions, or has an argument type that cannot + /// be parsed. + /// + /// + /// + /// Instead of this constructor, it's recommended to use the + /// class instead. + /// /// - /// The derived type also provides strongly-typed instance - /// methods, if you don't wish to use the static - /// method. + /// This constructor uses reflection to determine the arguments defined by the type indicated + /// by at runtime, unless the type has the + /// applied. For a type using that attribute, you can + /// also use the generated static or + /// methods on the arguments class instead. /// /// - /// The class can generate detailed usage help for the - /// defined arguments, which can be shown to the user to provide information about how to - /// invoke your application from the command line. This usage is shown automatically by the - /// method and the class, - /// or you can use the and methods to generate - /// it manually. + /// If the parameter is not , the + /// instance passed in will be modified to reflect the options from the arguments class's + /// attribute, if it has one. /// /// - /// The class is for applications with a single (root) command. - /// If you wish to create an application with subcommands, use the - /// class instead. + /// Certain properties of the class can be changed after the + /// class has been constructed, and still affect the + /// parsing behavior. See the property for details. + /// + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] +#endif + public CommandLineParser(Type argumentsType, ParseOptions? options = null) + : this(GetArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)), options), options) + { + } + + /// + /// Initializes a new instance of the class using the + /// specified arguments type and options. + /// + /// + /// The that defines the command line arguments. + /// + /// + /// The options that control parsing behavior, or to use the + /// default options. + /// + /// + /// is . + /// + /// + /// The cannot use for the command + /// line arguments, because it violates one of the rules concerning argument names or + /// positions, or has an argument type that cannot be parsed. + /// + /// + /// + /// This constructor supports source generation, and should not typically be used directly + /// by application code. + /// + /// + /// If the parameter is not , the + /// instance passed in will be modified to reflect the options from the arguments class's + /// attribute, if it has one. /// /// - /// The supports two sets of rules for how to parse arguments; - /// mode and mode. For - /// more details on these rules, please see - /// the documentation on GitHub. + /// Certain properties of the class can be changed after the + /// class has been constructed, and still affect the + /// parsing behavior. See the property for details. /// /// - /// - /// - /// - /// Usage documentation - public class CommandLineParser + /// + public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null) { - #region Nested types - - private sealed class CommandLineArgumentComparer : IComparer + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _parseOptions = options ?? new(); + var optionsAttribute = _provider.OptionsAttribute; + if (optionsAttribute != null) { - private readonly IComparer _stringComparer; + _parseOptions.Merge(optionsAttribute); + } - public CommandLineArgumentComparer(IComparer stringComparer) + _mode = _parseOptions.ModeOrDefault; + var comparison = _parseOptions.ArgumentNameComparisonOrDefault; + ArgumentNameComparison = comparison; + _argumentNamePrefixes = DetermineArgumentNamePrefixes(_parseOptions); + _nameValueSeparators = DetermineNameValueSeparators(_parseOptions); + var prefixInfos = _argumentNamePrefixes.Select(p => new PrefixInfo { Prefix = p, Short = true }); + if (_mode == ParsingMode.LongShort) + { + _longArgumentNamePrefix = _parseOptions.LongArgumentNamePrefixOrDefault; + if (string.IsNullOrWhiteSpace(_longArgumentNamePrefix)) { - _stringComparer = stringComparer; + throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); } - public int Compare(CommandLineArgument? x, CommandLineArgument? y) - { - if (x == null) - { - if (y == null) - { - return 0; - } - else - { - return -1; - } - } - else if (y == null) - { - return 1; - } - - // Positional arguments come before non-positional ones, and must be sorted by position - if (x.Position != null) - { - if (y.Position != null) - { - return x.Position.Value.CompareTo(y.Position.Value); - } - else - { - return -1; - } - } - else if (y.Position != null) - { - return 1; - } + var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; + prefixInfos = prefixInfos.Append(longInfo); + _argumentsByShortName = new(new CharComparer(comparison)); + } - // Non-positional required arguments come before optional arguments - if (x.IsRequired) - { - if (!y.IsRequired) - { - return -1; - } - // If both are required, sort by name - } - else if (y.IsRequired) - { - return 1; - } + _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); + _argumentsByName = new(new MemoryComparer(comparison)); - // Sort the rest by name - return _stringComparer.Compare(x.ArgumentName, y.ArgumentName); - } + var builder = ImmutableArray.CreateBuilder(); + _positionalArgumentCount = DetermineMemberArguments(builder); + DetermineAutomaticArguments(builder); + // Sort the member arguments in usage order (positional first, then required + // non-positional arguments, then the rest by name. + builder.Sort(new CommandLineArgumentComparer(comparison)); + if (builder.Count == builder.Capacity) + { + _arguments = builder.MoveToImmutable(); } - - private struct PrefixInfo + else { - public string Prefix { get; set; } - public bool Short { get; set; } + _arguments = builder.ToImmutable(); } - #endregion - - private readonly Type _argumentsType; - private readonly List _arguments = new(); - private readonly SortedDictionary _argumentsByName; - - // Uses string, even though short names are single char, so it can use the same comparer - // as _argumentsByName. - private readonly SortedDictionary? _argumentsByShortName; - private readonly ConstructorInfo _commandLineConstructor; - private readonly int _constructorArgumentCount; - private readonly int _positionalArgumentCount; - - private readonly ParseOptions _parseOptions; - private readonly ParsingMode _mode; - private readonly PrefixInfo[] _sortedPrefixes; - private readonly string[] _argumentNamePrefixes; - private readonly string? _longArgumentNamePrefix; - - private ReadOnlyCollection? _argumentsReadOnlyWrapper; - private ReadOnlyCollection? _argumentNamePrefixesReadOnlyWrapper; - private int _injectionIndex = -1; - - /// - /// Gets the default character used to separate the name and the value of an argument. - /// - /// - /// The default character used to separate the name and the value of an argument, which is ':'. - /// - /// - /// This constant is used as the default value of the property. - /// - /// - public const char DefaultNameValueSeparator = ':'; - - /// - /// Gets the default prefix used for long argument names if is - /// . - /// - /// - /// The default long argument name prefix, which is '--'. - /// - /// - /// - /// This constant is used as the default value of the - /// property. - /// - /// - public const string DefaultLongArgumentNamePrefix = "--"; - - /// - /// Event raised when an argument is parsed from the command line. - /// - /// - /// - /// If the event handler sets the property to , command line processing will stop immediately, - /// and the method will return . The - /// property will be set to automatically. - /// - /// - /// If the argument used and the argument's method - /// canceled parsing, the property will already be - /// true when the event is raised. In this case, the property - /// will not automatically be set to . - /// - /// - /// This event is invoked after the and properties have been set. - /// - /// - public event EventHandler? ArgumentParsed; - - /// - /// Event raised when a non-multi-value argument is specified more than once. - /// - /// - /// - /// Handling this event allows you to inspect the new value, and decide to keep the old - /// or new value. It also allows you to, for instance, print a warning for duplicate - /// arguments. - /// - /// - /// This even is only raised when the property is - /// . - /// - /// - public event EventHandler? DuplicateArgument; - - /// - /// Initializes a new instance of the class using the - /// specified arguments type and options. - /// - /// The of the class that defines the command line arguments. - /// - /// The options that control parsing behavior, or to use the - /// default options. - /// - /// - /// is . - /// - /// - /// The cannot use as the command line arguments type, - /// because it violates one of the rules concerning argument names or positions, or has an argument type that cannot - /// be parsed. - /// - /// - /// - /// If the parameter is not , the - /// instance passed in will be modified to reflect the options from the arguments class's - /// attribute, if it has one. - /// - /// - /// Certain properties of the class can be changed after the - /// class has been constructed, and still affect the - /// parsing behavior. See the property for details. - /// - /// - /// Some of the properties of the class, like anything related - /// to error output, are only used by the static - /// class and are not used here. - /// - /// - public CommandLineParser(Type argumentsType, ParseOptions? options = null) + VerifyPositionalArgumentRules(); + } + + /// + /// Gets the command line argument parsing rules used by the parser. + /// + /// + /// The for this parser. The default is + /// . + /// + /// + /// + public ParsingMode Mode => _mode; + + /// + /// Gets the argument name prefixes used by this instance. + /// + /// + /// A list of argument name prefixes. + /// + /// + /// + /// The argument name prefixes are used to distinguish argument names from positional argument + /// values in a command line. + /// + /// + /// If the property is , these are the + /// prefixes for short argument names. Use the property + /// to get the prefix for long argument names. + /// + /// + /// + /// + public ImmutableArray ArgumentNamePrefixes => _argumentNamePrefixes; + + /// + /// Gets the prefix to use for long argument names. + /// + /// + /// The prefix for long argument names, or if the + /// property is not . + /// + /// + /// + /// 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 . + /// + /// + /// + /// + public string? LongArgumentNamePrefix => _longArgumentNamePrefix; + + /// + /// Gets the type that was used to define the arguments. + /// + /// + /// The that was used to define the arguments. + /// + public Type ArgumentsType => _provider.ArgumentsType; + + /// + /// Gets the friendly name of the application for use in the version information. + /// + /// + /// The friendly name of the application. + /// + /// + /// + /// The friendly name is determined by checking for the + /// attribute first on the arguments type, then on the arguments type's assembly. If + /// neither exists, the is used. If that is not present + /// either, the assembly's name is used. + /// + /// + /// This name is only used in the output of the automatically created "-Version" + /// attribute. + /// + /// + public string ApplicationFriendlyName => _provider.ApplicationFriendlyName; + + /// + /// Gets a description that is used when generating usage information. + /// + /// + /// The description of the command line application. The default value is an empty string (""). + /// + /// + /// + /// 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. + /// + /// + public string Description => _provider.Description; + + /// + /// Gets the options used by this instance. + /// + /// + /// An instance of the class. + /// + /// + /// + /// If you change the value of the , , + /// , or + /// property, this will affect the behavior of this instance. The + /// other properties of the class are only used when the + /// class is constructed, so changing them afterwards will + /// have no effect. + /// + /// + public ParseOptions Options => _parseOptions; + + /// + /// Gets the culture used to convert command line argument values from their string representation to the argument type. + /// + /// + /// The culture used to convert command line argument values from their string representation to the argument type. The default value + /// is . + /// + /// + /// + /// Use the class to change this value. + /// + /// + /// + public CultureInfo Culture => _parseOptions.Culture; + + /// + /// Gets a value indicating whether duplicate arguments are allowed. + /// + /// + /// if it is allowed to supply non-multi-value arguments more than once; otherwise, . + /// The default value is . + /// + /// + /// + /// If the property is , a is thrown by the + /// method if an argument's value is supplied more than once. + /// + /// + /// If the property is , the last value supplied for the argument is used if it is supplied multiple times. + /// + /// + /// The property has no effect on multi-value or + /// dictionary arguments, which can always be supplied multiple times. + /// + /// + /// Use the or class to + /// change this value. + /// + /// + /// + /// + public bool AllowDuplicateArguments => _parseOptions.DuplicateArgumentsOrDefault != ErrorMode.Error; + + /// + /// Gets a value indicating whether the name and the value of an argument may be in separate + /// argument tokens. + /// + /// + /// if names and values can be in separate tokens; + /// if the characters specified in the property must be + /// used. The default value is . + /// + /// + /// + /// If the property is , the + /// value of an argument can be separated from its name either by using the characters + /// specified in the property, or by using white space (i.e. + /// by having a second argument that has the value). Given a named argument named "Sample", + /// the command lines -Sample:value and -Sample value are both valid and will + /// assign the value "value" to the argument. In the latter case, the values "-Sample" and + /// "value" will be two separate entry in the array with the unparsed + /// arguments. + /// + /// + /// If the property is , + /// only the characters specified in the property are + /// allowed to separate the value from the name. The command line -Sample:value still + /// assigns the value "value" to the argument, but for the command line `-Sample value` the + /// argument is considered not to have a value (which is only valid if + /// is ), and "value" is + /// considered to be the value for the next positional argument. + /// + /// + /// For switch arguments (the property is ), + /// only the characters specified in the property are allowed + /// to specify an explicit value regardless of the value of the + /// property. Given a switch argument named "Switch" the command line -Switch false + /// is interpreted to mean that the value of "Switch" is and the value of the + /// next positional argument is "false", even if the + /// property is . + /// + /// + /// Use the or class to + /// change this value. + /// + /// + /// + /// + public bool AllowWhiteSpaceValueSeparator => _parseOptions.AllowWhiteSpaceValueSeparatorOrDefault; + + /// + /// Gets the characters used to separate the name and the value of an argument. + /// + /// + /// The characters used to separate the name and the value of an argument. + /// + /// + /// + /// Use the or class to + /// change this value. + /// + /// + /// + /// + /// + public ImmutableArray NameValueSeparators => _nameValueSeparators; + + /// + /// Gets or sets a value that indicates whether usage help should be displayed if the + /// method returned . + /// + /// + /// if usage help should be displayed; otherwise, . + /// + /// + /// + /// Check this property after calling the method or one + /// of its overloads to see if usage help should be displayed. + /// + /// + /// This property will always be if the + /// method returned a non- value. + /// + /// + /// This property will always be if the + /// method threw a , or if an argument used + /// with the + /// property or the event. + /// + /// + /// If an argument that is defined by a method () cancels + /// parsing by returning or from the + /// method, this property is not automatically set to . + /// Instead, the method should explicitly set the property if it + /// wants usage help to be displayed. + /// + /// + /// [CommandLineArgument] + /// public static CancelMode MethodArgument(CommandLineParser parser) + /// { + /// parser.HelpRequested = true; + /// return CancelMode.Abort; + /// } + /// + /// + public bool HelpRequested { get; set; } + + /// + /// Gets the implementation used to get strings for + /// error messages and usage help. + /// + /// + /// An instance of a class inheriting from the class. + /// + /// + public LocalizedStringProvider StringProvider => _parseOptions.StringProvider; + + /// + /// Gets the class validators for the arguments class. + /// + /// + /// A list of instances. + /// + public IEnumerable Validators + => ArgumentsType.GetCustomAttributes(); + + /// + /// Gets the string comparison used for argument names. + /// + /// + /// One of the values of the enumeration. + /// + /// + /// + public StringComparison ArgumentNameComparison { get; } + + /// + /// Gets the arguments supported by this instance. + /// + /// + /// A list of all the arguments. + /// + /// + /// + /// The property can be used to retrieve additional information about the arguments, including their name, description, + /// and default value. Their current value can also be retrieved this way, in addition to using the arguments type directly. + /// + /// + /// To find an argument by name or alias, use the or + /// method. + /// + /// + public ImmutableArray Arguments => _arguments; + + /// + /// Gets the automatic help argument, or an argument with the same name, if there is one. + /// + /// + /// A instance, or if the automatic + /// help argument was disabled using the class or the + /// attribute. + /// + /// + /// + /// If the automatic help argument is enabled, this will return either the created help + /// argument, or the argument that conflicted with its name or one of its aliases, which is + /// assumed to be the argument used to display help in that case. + /// + /// + /// This is used the method to determine + /// whether to show the message and the actual name of the argument to use. + /// + /// + public CommandLineArgument? HelpArgument { get; private set; } + + /// + /// Gets the result of the last command line argument parsing operation. + /// + /// + /// An instance of the class. + /// + /// + /// + /// Use this property to get the name of the argument that canceled parsing, or to get + /// error information if the method returns + /// . + /// + /// + public ParseResult ParseResult { get; private set; } + + /// + /// Gets the kind of provider that was used to determine the available arguments. + /// + /// + /// One of the values of the enumeration. + /// + public ProviderKind ProviderKind => _provider.Kind; + + internal IComparer? ShortArgumentNameComparer => _argumentsByShortName?.Comparer; + + + /// + /// Gets the name of the executable used to invoke the application. + /// + /// + /// to include the file name extension in the result; otherwise, + /// . + /// + /// + /// The file name of the application's executable, with or without extension. + /// + /// + /// + /// To determine the executable name, this method first checks the + /// property (if using .Net 6.0 or later). If using the .Net Standard package, or if + /// returns "dotnet", it checks the first item in + /// the array returned by , and finally falls + /// back to the file name of the entry point assembly. + /// + /// + /// The return value of this function is used as the default executable name to show in + /// the usage syntax when generating usage help, unless overridden by the + /// property. + /// + /// + /// + public static string GetExecutableName(bool includeExtension = false) + { + string? path = null; + string? nameWithoutExtension = null; +#if NET6_0_OR_GREATER + // Prefer this because it actually returns the exe name, not the dll. + path = Environment.ProcessPath; + + // Fall back if this returned the dotnet executable. + nameWithoutExtension = Path.GetFileNameWithoutExtension(path); + if (nameWithoutExtension == "dotnet") + { + path = null; + nameWithoutExtension = null; + } +#endif + path ??= Environment.GetCommandLineArgs().FirstOrDefault() ?? Assembly.GetEntryAssembly()?.Location; + if (path == null) + { + path = string.Empty; + } + else if (includeExtension) + { + path = Path.GetFileName(path); + } + else { - _argumentsType = argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)); - _parseOptions = options ?? new(); + path = nameWithoutExtension ?? Path.GetFileNameWithoutExtension(path); + } - var optionsAttribute = _argumentsType.GetCustomAttribute(); - if (optionsAttribute != null) - { - _parseOptions.Merge(optionsAttribute); - } + return path; + } - _mode = _parseOptions.Mode ?? default; - var comparer = _parseOptions.ArgumentNameComparer ?? StringComparer.OrdinalIgnoreCase; - _argumentNamePrefixes = DetermineArgumentNamePrefixes(_parseOptions); - var prefixInfos = _argumentNamePrefixes.Select(p => new PrefixInfo { Prefix = p, Short = true }); - if (_mode == ParsingMode.LongShort) - { - _longArgumentNamePrefix = _parseOptions.LongArgumentNamePrefix ?? DefaultLongArgumentNamePrefix; - if (string.IsNullOrWhiteSpace(_longArgumentNamePrefix)) - { - throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); - } + /// + /// Writes command line usage help using the specified instance. + /// + /// + /// The to use to create the usage. If , + /// the value from the property in the + /// property is sued. + /// + /// + /// + /// The usage help consists of first the , followed by the usage + /// syntax, followed by a description of all the arguments. + /// + /// + /// You can add descriptions to the usage text by applying the + /// attribute to your command line arguments type, and the properties and methods defining + /// command line arguments. + /// + /// + /// Color is applied to the output only if the instance + /// has enabled it. + /// + /// + /// + public void WriteUsage(UsageWriter? usageWriter = null) + { + usageWriter ??= _parseOptions.UsageWriter; + usageWriter.WriteParserUsage(this); + } - var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; - prefixInfos = prefixInfos.Append(longInfo); - _argumentsByShortName = new(comparer); - } + /// + /// Gets a string containing command line usage help. + /// + /// + /// The maximum line length of lines in the usage text. A value less than 1 or larger + /// than 65536 is interpreted as infinite line length. + /// + /// + /// The to use to create the usage. If , + /// the value from the property in the + /// property is used. + /// + /// + /// A string containing usage help for the command line options defined by the type + /// specified by . + /// + /// + /// + /// + public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = 0) + { + usageWriter ??= _parseOptions.UsageWriter; + return usageWriter.GetUsage(this, maximumLineLength: maximumLineLength); + } - _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); - _argumentsByName = new(comparer); + /// + /// Parses the arguments returned by the + /// method. + /// + /// + /// An instance of the type specified by the property, or + /// if argument parsing was canceled by the + /// event handler, the property, + /// or a method argument that returned or + /// . + /// + /// + /// + /// If the return value is , check the + /// property to see if usage help should be displayed. + /// + /// + /// + /// An error occurred parsing the command line. Check the + /// property for the exact reason for the error. + /// + public object? Parse() + { + // GetCommandLineArgs include the executable, so skip it. + return Parse(Environment.GetCommandLineArgs().AsMemory(1)); + } - _commandLineConstructor = GetCommandLineConstructor(); - DetermineConstructorArguments(); - _constructorArgumentCount = _arguments.Count; - _positionalArgumentCount = _constructorArgumentCount + DetermineMemberArguments(options, optionsAttribute); - DetermineAutomaticArguments(options, optionsAttribute); - if (_arguments.Count > _constructorArgumentCount) - { - // Sort the member arguments in usage order (positional first, then required - // non-positional arguments, then the rest by name. - _arguments.Sort(_constructorArgumentCount, _arguments.Count - _constructorArgumentCount, new CommandLineArgumentComparer(_argumentsByName.Comparer)); - } + /// + /// + /// Parses the specified command line arguments. + /// + /// The command line arguments. + /// + /// is . + /// + public object? Parse(string[] args) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + return Parse(args.AsMemory()); + } - VerifyPositionalArgumentRules(); + /// + /// + /// Parses the specified command line arguments. + /// + /// The command line arguments. + public object? Parse(ReadOnlyMemory args) + { + int index = -1; + try + { + HelpRequested = false; + return ParseCore(args, ref index); + } + catch (CommandLineArgumentException ex) + { + HelpRequested = true; + ParseResult = ParseResult.FromException(ex, args.Slice(index)); + throw; } + } + + /// + /// Parses the arguments returned by the + /// method, and displays error messages and usage help if required. + /// + /// + /// An instance of the type specified by the property, or + /// if an error occurred, or argument parsing was canceled by the + /// property or a method argument + /// that returned or . + /// + /// + /// + /// If an error occurs or parsing is canceled, it prints errors to the + /// stream, and usage help using the if the + /// property is . It then returns . + /// + /// + /// If the return value is , check the + /// property for more information about whether an error occurred or parsing was + /// canceled. + /// + /// + /// This method will never throw a exception. + /// + /// + public object? ParseWithErrorHandling() + { + // GetCommandLineArgs includes the executable, so skip it. + return ParseWithErrorHandling(Environment.GetCommandLineArgs().AsMemory(1)); + } - /// - /// Gets the command line argument parsing rules used by the parser. - /// - /// - /// The for this parser. The default is - /// . - /// - /// - /// - public ParsingMode Mode => _mode; - - /// - /// Gets the argument name prefixes used by this instance. - /// - /// - /// A list of argument name prefixes. - /// - /// - /// - /// The argument name prefixes are used to distinguish argument names from positional argument values in a command line. - /// - /// - /// These prefixes will be used for short argument names if the - /// property is . Use - /// to get the prefix for long argument names. - /// - /// - /// - /// - public ReadOnlyCollection ArgumentNamePrefixes => - _argumentNamePrefixesReadOnlyWrapper ??= new(_argumentNamePrefixes); - - /// - /// Gets the prefix to use for long argument names. - /// - /// - /// The prefix for long argument names, or if - /// is not . - /// - /// - /// - /// The long argument prefix is only used if property is - /// . See to - /// get the prefixes for short argument names. - /// - /// - /// - /// - public string? LongArgumentNamePrefix => _longArgumentNamePrefix; - - /// - /// Gets the type that was used to define the arguments. - /// - /// - /// The that was used to define the arguments. - /// - public Type ArgumentsType + /// + /// + /// Parses the specified command line arguments and displays error messages and usage help if + /// required. + /// + /// The command line arguments. + /// + /// is . + /// + public object? ParseWithErrorHandling(string[] args) + { + if (args == null) { - get { return _argumentsType; } + throw new ArgumentNullException(nameof(args)); } - /// - /// Gets the friendly name of the application. - /// - /// - /// The friendly name of the application. - /// - /// - /// - /// The friendly name is determined by checking for the - /// attribute first on the arguments type, then on the arguments type's assembly. If - /// neither exists, the arguments type's assembly's name is used. - /// - /// - /// This name is only used in the output of the automatically created "-Version" - /// attribute. - /// - /// - public string ApplicationFriendlyName + return ParseWithErrorHandling(args.AsMemory()); + } + + /// + /// + /// Parses the specified command line arguments, and displays error messages and usage help if + /// required. + /// + /// The command line arguments. + public object? ParseWithErrorHandling(ReadOnlyMemory args) + { + EventHandler? handler = null; + if (_parseOptions.DuplicateArgumentsOrDefault == ErrorMode.Warning) { - get + handler = (sender, e) => { - var attribute = _argumentsType.GetCustomAttribute() ?? - _argumentsType.Assembly.GetCustomAttribute(); + var warning = StringProvider.DuplicateArgumentWarning(e.Argument.ArgumentName); + WriteError(_parseOptions, warning, _parseOptions.WarningColor); + }; + + DuplicateArgument += handler; + } - return attribute?.Name ?? _argumentsType.Assembly.GetName().Name ?? string.Empty; + var helpMode = UsageHelpRequest.Full; + object? result = null; + try + { + result = Parse(args); + } + catch (CommandLineArgumentException ex) + { + WriteError(_parseOptions, ex.Message, _parseOptions.ErrorColor, true); + helpMode = _parseOptions.ShowUsageOnError; + } + finally + { + if (handler != null) + { + DuplicateArgument -= handler; } } - /// - /// Gets a description that is used when generating usage information. - /// - /// - /// The description of the command line application. The default value is an empty string (""). - /// - /// - /// - /// 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. - /// - /// - public string Description - => _argumentsType.GetCustomAttribute()?.Description ?? string.Empty; - - /// - /// Gets the options used by this instance. - /// - /// - /// An instance of the class. - /// - /// - /// - /// If you change the value of the , , - /// , , - /// or property, this will affect - /// the behavior of this instance. The other properties of the - /// class are only used when the class in constructed, so - /// changing them afterwards will have no effect. - /// - /// - public ParseOptions Options => _parseOptions; - - /// - /// Gets the culture used to convert command line argument values from their string representation to the argument type. - /// - /// - /// The culture used to convert command line argument values from their string representation to the argument type. The default value - /// is . - /// - /// - /// - /// Use the class to change this value. - /// - /// - /// - public CultureInfo Culture => _parseOptions.Culture ?? CultureInfo.InvariantCulture; - - /// - /// Gets a value indicating whether duplicate arguments are allowed. - /// - /// - /// if it is allowed to supply non-multi-value arguments more than once; otherwise, . - /// The default value is . - /// - /// - /// - /// If the property is , a is thrown by the - /// method if an argument's value is supplied more than once. - /// - /// - /// If the property is , the last value supplied for the argument is used if it is supplied multiple times. - /// - /// - /// The property has no effect on multi-value or - /// dictionary arguments, which can always be supplied multiple times. - /// - /// - /// Use the or class to - /// change this value. - /// - /// - /// - /// - public bool AllowDuplicateArguments => (_parseOptions.DuplicateArguments ?? default) != ErrorMode.Error; - - /// - /// Gets value indicating whether the value of an argument may be in a separate - /// argument from its name. - /// - /// - /// if names and values can be in separate arguments; if the character - /// specified in the property must be used. The default - /// value is . - /// - /// - /// - /// If the property is , - /// the value of an argument can be separated from its name either by using the character - /// specified in the property or by using white space (i.e. - /// by having a second argument that has the value). Given a named argument named "Sample", - /// the command lines -Sample:value and -Sample value - /// are both valid and will assign the value "value" to the argument. - /// - /// - /// If the property is , only the character - /// specified in the property is allowed to separate the value from the name. - /// The command line -Sample:value still assigns the value "value" to the argument, but for the command line "-Sample value" the argument - /// is considered not to have a value (which is only valid if is ), and - /// "value" is considered to be the value for the next positional argument. - /// - /// - /// For switch arguments (the property is ), - /// only the character specified in the property is allowed - /// to specify an explicit value regardless of the value of the - /// property. Given a switch argument named "Switch" the command line -Switch false - /// is interpreted to mean that the value of "Switch" is and the value of the - /// next positional argument is "false", even if the - /// property is . - /// - /// - /// Use the or class to - /// change this value. - /// - /// - /// - /// - public bool AllowWhiteSpaceValueSeparator => _parseOptions.AllowWhiteSpaceValueSeparator ?? true; - - /// - /// Gets or sets the character used to separate the name and the value of an argument. - /// - /// - /// The character used to separate the name and the value of an argument. The default value is the - /// constant, a colon (:). - /// - /// - /// - /// This character is used to separate the name and the value if both are provided as - /// a single argument to the application, e.g. -sample:value if the default value is used. - /// - /// - /// The character chosen here cannot be used in the name of any parameter. Therefore, - /// it's usually best to choose a non-alphanumeric value such as the colon or equals sign. - /// The character can appear in argument values (e.g. -sample:foo:bar is fine, in which - /// case the value is "foo:bar"). - /// - /// - /// Do not pick a whitespace character as the separator. Doing this only works if the - /// whitespace character is part of the argument token, which usually means it needs to be - /// quoted or escaped when invoking your application. Instead, use the - /// property to control whether whitespace - /// is allowed as a separator. - /// - /// - /// Use the or class to - /// change this value. - /// - /// - /// - /// - public char NameValueSeparator => _parseOptions.NameValueSeparator ?? DefaultNameValueSeparator; - - /// - /// Gets or sets a value that indicates whether usage help should be displayed if the - /// method returned . - /// - /// - /// if usage help should be displayed; otherwise, . - /// - /// - /// - /// Check this property after calling the method - /// to see if usage help should be displayed. - /// - /// - /// This property will be if the - /// method threw a , if an argument used - /// , if parsing was canceled - /// using the event. - /// - /// - /// If an argument that is defined by a method () cancels - /// parsing by returning from the method, this property is not - /// automatically set to . Instead, the method should explicitly - /// set the property if it wants usage help to be displayed. - /// - /// - /// [CommandLineArgument] - /// public static bool MethodArgument(CommandLineParser parser) - /// { - /// parser.HelpRequested = true; - /// return false; - /// } - /// - /// - /// The property will always be if - /// did not throw and returned a non-null value. - /// - /// - public bool HelpRequested { get; set; } - - /// - /// Gets the implementation used to get strings for - /// error messages and usage help. - /// - /// - /// An instance of a class inheriting from the class. - /// - /// - public LocalizedStringProvider StringProvider => _parseOptions.StringProvider; - - /// - /// Gets the class validators for the arguments class. - /// - /// - /// A list of instances. - /// - public IEnumerable Validators - => ArgumentsType.GetCustomAttributes(); - - /// - /// Gets the string comparer used for argument names. - /// - /// - /// An instance of a class implementing the interface. - /// - /// - /// - public IComparer ArgumentNameComparer => _argumentsByName.Comparer; - - /// - /// Gets the arguments supported by this instance. - /// - /// - /// A list of all the arguments. - /// - /// - /// - /// The property can be used to retrieve additional information about the arguments, including their name, description, - /// and default value. Their current value can also be retrieved this way, in addition to using the arguments type directly. - /// - /// - public ReadOnlyCollection Arguments => _argumentsReadOnlyWrapper ??= _arguments.AsReadOnly(); - - /// - /// Gets the automatic help argument or an argument with the same name, if there is one. - /// - /// - /// A instance, or if there is no - /// argument using the name of the automatic help argument. - /// - public CommandLineArgument? HelpArgument { get; private set; } - - /// - /// Gets the result of the last call to the method. - /// - /// - /// An instance of the class. - /// - /// - /// - /// Use this property to get the name of the argument that canceled parsing, or to get - /// error information if the method returns - /// . - /// - /// - public ParseResult ParseResult { get; private set; } - - /// - /// Gets the name of the executable used to invoke the application. - /// - /// - /// to include the file name extension in the result; otherwise, - /// . - /// - /// - /// The file name of the application's executable, with or without extension. - /// - /// - /// - /// To determine the executable name, this method first checks the - /// property (if using .Net 6.0 or later). If using the .Net Standard package, or if - /// returns "dotnet", it checks the first item in - /// the array returned by , and finally falls - /// back to the file name of the entry point assembly. - /// - /// - /// The return value of this function is used as the default executable name to show in - /// the usage syntax when generating usage help, unless overridden by the - /// property. - /// - /// - /// - public static string GetExecutableName(bool includeExtension = false) + if (HelpRequested) { - string? path = null; - string? nameWithoutExtension = null; + _parseOptions.UsageWriter.WriteParserUsage(this, helpMode); + } + + return result; + } + + /// + /// Parses the arguments returned by the + /// method using the type . + /// + /// The type defining the command line arguments. + /// + /// The options that control parsing behavior and usage help formatting. If + /// , the default options are used. + /// + /// + /// An instance of the type , or if an + /// error occurred, or argument parsing was canceled by the + /// property or a method argument that returned + /// or . + /// + /// + /// + /// + /// + /// The cannot use as the command + /// line arguments type, because it violates one of the rules concerning argument names or + /// positions, or has an argument type that cannot be parsed. + /// + /// + /// + /// This is a convenience function that instantiates a , + /// calls the method, and returns + /// the result. If an error occurs or parsing is canceled, it prints errors to the + /// stream, and usage help to the + /// if the property is . + /// It then returns . + /// + /// + /// If the parameter is , output is + /// written to a for the standard error stream, + /// wrapping at the console's window width. If the stream is redirected, output may still + /// be wrapped, depending on the value returned by . + /// + /// + /// Color is applied to the output depending on the value of the + /// property, the property, and the capabilities + /// of the console. + /// + /// + /// If you want more control over the parsing process, including custom error/usage output + /// or handling the event, you should use the + /// instance or + /// method. + /// + /// + /// This method uses reflection to determine the arguments defined by the type + /// at runtime, unless the type has the applied. For a + /// type using that attribute, you can also use the generated static + /// or + /// methods on the + /// arguments class instead. + /// + /// #if NET6_0_OR_GREATER - // Prefer this because it actually returns the exe name, not the dll. - path = Environment.ProcessPath; + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] +#endif + public static T? Parse(ParseOptions? options = null) + where T : class + { + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(); + } - // Fall back if this returned the dotnet executable. - nameWithoutExtension = Path.GetFileNameWithoutExtension(path); - if (nameWithoutExtension == "dotnet") - { - path = null; - nameWithoutExtension = null; - } + /// + /// Parses the specified command line arguments using the type . + /// + /// The type defining the command line arguments. + /// The command line arguments. + /// + /// The options that control parsing behavior and usage help formatting. If + /// , the default options are used. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] #endif - path ??= Environment.GetCommandLineArgs().FirstOrDefault() ?? Assembly.GetEntryAssembly()?.Location; - if (path == null) - { - path = string.Empty; - } - else if (includeExtension) - { - path = Path.GetFileName(path); - } - else - { - path = nameWithoutExtension ?? Path.GetFileNameWithoutExtension(path); - } + public static T? Parse(ReadOnlyMemory args, ParseOptions? options = null) + where T : class + { + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(args); + } - return path; - } - /// - /// Writes command line usage help to the specified using the specified options. - /// - /// - /// The to use to create the usage. If , - /// the value from the property in the - /// property is sued. - /// - /// - /// - /// The usage help consists of first the , followed by the usage syntax, followed by a description of all the arguments. - /// - /// - /// You can add descriptions to the usage text by applying the attribute to your command line arguments type, - /// and the constructor parameters and properties defining command line arguments. - /// - /// - /// Color is applied to the output only if the instance - /// has enabled it. - /// - /// - /// - public void WriteUsage(UsageWriter? usageWriter = null) + /// + /// Parses the specified command line arguments using the type . + /// + /// The type defining the command line arguments. + /// The command line arguments. + /// + /// The options that control parsing behavior and usage help formatting. If + /// , the default options are used. + /// + /// + /// + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] +#endif + public static T? Parse(string[] args, ParseOptions? options = null) + where T : class + { + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(args); + } + + /// + /// Gets a command line argument by name or alias. + /// + /// The name or alias of the argument. + /// The instance containing information about + /// the argument, or if the argument was not found. + /// is . + /// + /// + /// If the property is , this uses + /// the long name and long aliases of the argument. + /// + /// + /// This method only uses the actual names and aliases; it does not consider auto prefix + /// aliases regardless of the value of the + /// property. + /// + /// + public CommandLineArgument? GetArgument(string name) + { + if (name == null) { - usageWriter ??= _parseOptions.UsageWriter; - usageWriter.WriteParserUsage(this); + throw new ArgumentNullException(nameof(name)); } - /// - /// Gets a string containing command line usage help. - /// - /// - /// The maximum line length of lines in the usage text. A value less than 1 or larger - /// than 65536 is interpreted as infinite line length. - /// - /// - /// The to use to create the usage. If , - /// the value from the property in the - /// property is sued. - /// - /// - /// A string containing usage help for the command line options defined by the type - /// specified by . - /// - /// - /// - /// - public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = 0) + if (_argumentsByName.TryGetValue(name.AsMemory(), out var argument)) { - usageWriter ??= _parseOptions.UsageWriter; - return usageWriter.GetUsage(this, maximumLineLength: maximumLineLength); + return argument; } + else + { + return null; + } + } - /// - /// Parses the arguments returned by the - /// method. - /// - /// - /// An instance of the type specified by the property, or - /// if argument parsing was canceled by the - /// event handler, the property, - /// or a method argument that returned . - /// - /// - /// - /// If the return value is , check the - /// property to see if usage help should be displayed. - /// - /// - /// - /// An error occurred parsing the command line. Check the - /// property for the exact reason for the error. - /// - public object? Parse() + /// + /// Gets a command line argument by short name or alias. + /// + /// The short name of the argument. + /// The instance containing information about + /// the argument, or if the argument was not found. + /// + /// + /// If is not , this + /// method always returns + /// + /// + public CommandLineArgument? GetShortArgument(char shortName) + { + if (_argumentsByShortName != null && _argumentsByShortName.TryGetValue(shortName, out var argument)) { - // GetCommandLineArgs include the executable, so skip it. - return Parse(Environment.GetCommandLineArgs(), 1); + return argument; } + else + { + return null; + } + } + + /// + /// Gets the default argument name prefixes for the current platform. + /// + /// + /// An array containing the default prefixes for the current platform. + /// + /// + /// + /// The default prefixes for each platform are: + /// + /// + /// + /// Platform + /// Prefixes + /// + /// + /// Windows + /// '-' and '/' + /// + /// + /// Other + /// '-' + /// + /// + /// + /// If the property is , these + /// prefixes will be used for short argument names. The + /// constant is the default prefix for long argument names regardless of platform. + /// + /// + /// + /// + /// + public static ImmutableArray GetDefaultArgumentNamePrefixes() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? ImmutableArray.Create("-", "/") + : ImmutableArray.Create("-"); + } + + /// + /// Gets the default characters used to separate the name and the value of an argument. + /// + /// + /// The default characters used to separate the name and the value of an argument, which are + /// ':' and '='. + /// + /// + /// The return value of this method is used as the default value of the property. + /// + /// + public static ImmutableArray GetDefaultNameValueSeparators() => ImmutableArray.Create(':', '='); + + /// + /// Raises the event. + /// + /// The data for the event. + 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); + } + + internal static bool ShouldIndent(LineWrappingTextWriter writer) + { + return writer.MaximumLineLength is 0 or >= 30; + } - /// - /// - /// Parses the specified command line arguments, starting at the specified index. - /// - /// The command line arguments. - /// The index of the first argument to parse. - /// - /// is . - /// - /// - /// does not fall within the bounds of . - /// - public object? Parse(string[] args, int index = 0) + internal static void WriteError(ParseOptions options, string message, TextFormat color, bool blankLine = false) + { + using var errorVtSupport = options.EnableErrorColor(); + try { - try - { - HelpRequested = false; - return ParseCore(args, index); - } - catch (CommandLineArgumentException ex) + using var error = DisposableWrapper.Create(options.Error, LineWrappingTextWriter.ForConsoleError); + if (options.UseErrorColor ?? false) { - HelpRequested = true; - ParseResult = ParseResult.FromException(ex); - throw; + error.Inner.Write(color); } - } - - /// - /// Parses the arguments returned by the - /// method, and displays error messages and usage help if required. - /// - /// - /// An instance of the type specified by the property, or - /// if an error occurred, or argument parsing was canceled by the - /// property or a method argument - /// that returned . - /// - /// - /// - /// If an error occurs or parsing is canceled, it prints errors to the stream, and usage help to the if - /// the property is . It then returns - /// . - /// - /// - /// If the return value is , check the - /// property for more information about whether an error occurred or parsing was - /// canceled. - /// - /// - /// This method will never throw a exception. - /// - /// - public object? ParseWithErrorHandling() - { - // GetCommandLineArgs include the executable, so skip it. - return ParseWithErrorHandling(Environment.GetCommandLineArgs(), 1); - } - /// - /// - /// Parses the specified command line arguments, starting at the specified index, and - /// displays error messages and usage help if required. - /// - /// The command line arguments. - /// The index of the first argument to parse. - /// - /// is . - /// - /// - /// does not fall within the bounds of . - /// - public object? ParseWithErrorHandling(string[] args, int index = 0) - { - EventHandler? handler = null; - if (_parseOptions.DuplicateArguments == ErrorMode.Warning) + error.Inner.Write(message); + if (options.UseErrorColor ?? false) { - handler = (sender, e) => - { - var warning = StringProvider.DuplicateArgumentWarning(e.Argument.ArgumentName); - WriteError(_parseOptions, warning, _parseOptions.WarningColor); - }; - - DuplicateArgument += handler; + error.Inner.Write(options.UsageWriter.ColorReset); } - var helpMode = UsageHelpRequest.Full; - object? result = null; - try + error.Inner.WriteLine(); + if (blankLine) { - result = Parse(args, index); + error.Inner.WriteLine(); } - catch (CommandLineArgumentException ex) + } + finally + { + // Reset UseErrorColor if it was changed. + if (errorVtSupport != null) { - WriteError(_parseOptions, ex.Message, _parseOptions.ErrorColor, true); - helpMode = _parseOptions.ShowUsageOnError; + options.UseErrorColor = null; } - finally + } + } + + private static ImmutableArray DetermineArgumentNamePrefixes(ParseOptions options) + { + if (options.ArgumentNamePrefixes == null) + { + return GetDefaultArgumentNamePrefixes(); + } + else + { + var result = options.ArgumentNamePrefixes.ToImmutableArray(); + if (result.Length == 0) { - if (handler != null) - { - DuplicateArgument -= handler; - } + throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefixes, nameof(options)); } - if (HelpRequested) + if (result.Any(prefix => string.IsNullOrWhiteSpace(prefix))) { - _parseOptions.UsageWriter.WriteParserUsage(this, helpMode); + throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); } return result; } + } - /// - /// Parses the arguments returned by the - /// method using the type . - /// - /// The type defining the command line arguments. - /// - /// The options that control parsing behavior and usage help formatting. If - /// , the default options are used. - /// - /// - /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the - /// property or a method argument that returned . - /// - /// - /// - /// - /// - /// - /// This is a convenience function that instantiates a , - /// calls the method, and returns the result. If an error occurs - /// or parsing is canceled, it prints errors to the - /// stream, and usage help to the if the - /// property is . It then returns . - /// - /// - /// If the parameter is , output is - /// written to a for the standard error stream, - /// wrapping at the console's window width. If the stream is redirected, output may still - /// be wrapped, depending on the value returned by . - /// - /// - /// Color is applied to the output depending on the value of the - /// property, the property, and the capabilities - /// of the console. - /// - /// - /// If you want more control over the parsing process, including custom error/usage output - /// or handling the event, you should manually create an - /// instance of the class and call its - /// method. - /// - /// - public static T? Parse(ParseOptions? options = null) - where T : class + private static ImmutableArray DetermineNameValueSeparators(ParseOptions options) + { + if (options.NameValueSeparators == null) { - // GetCommandLineArgs include the executable, so skip it. - return Parse(Environment.GetCommandLineArgs(), 1, options); + return GetDefaultNameValueSeparators(); } - - /// - /// Parses the specified command line arguments, starting at the specified index, using the - /// type . - /// - /// The type defining the command line arguments. - /// The command line arguments. - /// The index of the first argument to parse. - /// - /// The options that control parsing behavior and usage help formatting. If - /// , the default options are used. - /// - /// - /// - /// - /// - /// is . - /// - /// - /// does not fall within the bounds of . - /// - /// - /// - /// - /// - /// - /// - public static T? Parse(string[] args, int index, ParseOptions? options = null) - where T : class + else { - return (T?)ParseInternal(typeof(T), args, index, options); - } + var result = options.NameValueSeparators.ToImmutableArray(); + if (result.Length == 0) + { + throw new ArgumentException(Properties.Resources.EmptyNameValueSeparators, nameof(options)); + } - /// - /// Parses the specified command line arguments using the type . - /// - /// The type defining the command line arguments. - /// The command line arguments. - /// - /// The options that control parsing behavior and usage help formatting. If - /// , the default options are used. - /// - /// - /// - /// - /// - /// is . - /// - /// - /// - /// - /// - /// - /// - public static T? Parse(string[] args, ParseOptions? options = null) - where T : class - { - return Parse(args, 0, options); + return result; } + } - /// - /// Gets a command line argument by name or alias. - /// - /// The name or alias of the argument. - /// The instance containing information about - /// the argument, or if the argument was not found. - /// is . - /// - /// If the property is , this uses - /// the long name and long aliases of the argument. - /// - public CommandLineArgument? GetArgument(string name) + private int DetermineMemberArguments(ImmutableArray.Builder builder) + { + int additionalPositionalArgumentCount = 0; + foreach (var argument in _provider.GetArguments(this)) { - if (name == null) + AddNamedArgument(argument, builder); + if (argument.Position != null) { - throw new ArgumentNullException(nameof(name)); + ++additionalPositionalArgumentCount; } + } - if (_argumentsByName.TryGetValue(name, out var argument)) - { - return argument; - } - else + return additionalPositionalArgumentCount; + } + + private void DetermineAutomaticArguments(ImmutableArray.Builder builder) + { + bool autoHelp = Options.AutoHelpArgumentOrDefault; + if (autoHelp) + { + var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this); + + if (created) { - return null; + AddNamedArgument(argument, builder); } + + HelpArgument = argument; } - /// - /// Gets a command line argument by short name. - /// - /// The short name of the argument. - /// The instance containing information about - /// the argument, or if the argument was not found. - /// - /// - /// If is not , this - /// method always returns - /// - /// - public CommandLineArgument? GetShortArgument(char shortName) + bool autoVersion = Options.AutoVersionArgumentOrDefault; + if (autoVersion && !_provider.IsCommand) { - if (_argumentsByShortName != null && _argumentsByShortName.TryGetValue(shortName.ToString(), out var argument)) - { - return argument; - } - else + var argument = CommandLineArgument.CreateAutomaticVersion(this); + + if (argument != null) { - return null; + AddNamedArgument(argument, builder); } } + } - /// - /// Gets the default argument name prefixes for the current platform. - /// - /// - /// An array containing the default prefixes for the current platform. - /// - /// - /// - /// The default prefixes for each platform are: - /// - /// - /// - /// Platform - /// Prefixes - /// - /// - /// Windows - /// '-' and '/' - /// - /// - /// Other - /// '-' - /// - /// - /// - /// If the property is , these - /// prefixes will be used for short argument names. The - /// constant is the default prefix for long argument names regardless of platform. - /// - /// - /// - /// - /// - public static string[] GetDefaultArgumentNamePrefixes() + private void AddNamedArgument(CommandLineArgument argument, ImmutableArray.Builder builder) + { + if (_nameValueSeparators.Any(separator => argument.ArgumentName.Contains(separator))) { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? new[] { "-", "/" } - : new[] { "-" }; + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.ArgumentNameContainsSeparatorFormat, argument.ArgumentName)); } - /// - /// Raises the event. - /// - /// The data for the event. - protected virtual void OnArgumentParsed(ArgumentParsedEventArgs e) + if (argument.HasLongName) { - ArgumentParsed?.Invoke(this, e); + _argumentsByName.Add(argument.ArgumentName.AsMemory(), argument); + foreach (string alias in argument.Aliases) + { + _argumentsByName.Add(alias.AsMemory(), argument); + } } - /// - /// Raises the event. - /// - /// The data for the event. - protected virtual void OnDuplicateArgument(DuplicateArgumentEventArgs e) + if (_argumentsByShortName != null && argument.HasShortName) { - DuplicateArgument?.Invoke(this, e); + _argumentsByShortName.Add(argument.ShortName, argument); + foreach (var alias in argument.ShortAliases) + { + _argumentsByShortName.Add(alias, argument); + } } - internal static object? ParseInternal(Type argumentsType, string[] args, int index, ParseOptions? options) + // The generated provider needs values for arguments that use a required property to be + // supplied to the CreateInstance method in the exact order they were originally returned, + // so a separate list is maintained for that. The reflection provider doesn't need these + // values at all. + if (_provider.Kind != ProviderKind.Reflection && argument.IsRequiredProperty) { - options ??= new(); - var parser = new CommandLineParser(argumentsType, options); - return parser.ParseWithErrorHandling(args, index); + _requiredPropertyArguments ??= new(); + _requiredPropertyArguments.Add(argument); } - internal static bool ShouldIndent(LineWrappingTextWriter writer) - { - return writer.MaximumLineLength is 0 or >= 30; - } + builder.Add(argument); + } - private static void WriteError(ParseOptions options, string message, string color, bool blankLine = false) - { - using var errorVtSupport = options.EnableErrorColor(); - try - { - using var error = DisposableWrapper.Create(options.Error, LineWrappingTextWriter.ForConsoleError); - if (options.UseErrorColor ?? false) - { - error.Inner.Write(color); - } + private void VerifyPositionalArgumentRules() + { + bool hasOptionalArgument = false; + bool hasMultiValueArgument = false; - error.Inner.Write(message); - if (options.UseErrorColor ?? false) - { - error.Inner.Write(options.UsageWriter.ColorReset); - } + for (int x = 0; x < _positionalArgumentCount; ++x) + { + CommandLineArgument argument = _arguments[x]; - error.Inner.WriteLine(); - if (blankLine) - { - error.Inner.WriteLine(); - } - } - finally + if (hasMultiValueArgument) { - // Reset UseErrorColor if it was changed. - if (errorVtSupport != null) - { - options.UseErrorColor = null; - } + throw new NotSupportedException(Properties.Resources.ArrayNotLastArgument); } - } - private static string[] DetermineArgumentNamePrefixes(ParseOptions options) - { - if (options.ArgumentNamePrefixes == null) + if (argument.IsRequired && hasOptionalArgument) { - return GetDefaultArgumentNamePrefixes(); + throw new NotSupportedException(Properties.Resources.InvalidOptionalArgumentOrder); } - else - { - var result = options.ArgumentNamePrefixes.ToArray(); - if (result.Length == 0) - { - throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefixes, nameof(options)); - } - if (result.Any(prefix => string.IsNullOrWhiteSpace(prefix))) - { - throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); - } + if (!argument.IsRequired) + { + hasOptionalArgument = true; + } - return result; + if (argument.MultiValueInfo != null) + { + hasMultiValueArgument = true; } + + argument.Position = x; } + } - private void DetermineConstructorArguments() + private object? ParseCore(ReadOnlyMemory args, ref int x) + { + // Reset all arguments to their default value. + foreach (CommandLineArgument argument in _arguments) { - ParameterInfo[] parameters = _commandLineConstructor.GetParameters(); - var valueDescriptionTransform = _parseOptions.ValueDescriptionTransform ?? default; + argument.Reset(); + } + + HelpRequested = false; + int positionalArgumentIndex = 0; - foreach (ParameterInfo parameter in parameters) + 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) { - if (parameter.ParameterType == typeof(CommandLineParser) && _injectionIndex < 0) - { - _injectionIndex = _arguments.Count; - } - else + // 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) { - var argument = CommandLineArgument.Create(this, parameter); - AddNamedArgument(argument); + break; } } - } - - private int DetermineMemberArguments(ParseOptions? options, ParseOptionsAttribute? optionsAttribute) - { - var valueDescriptionTransform = options?.ValueDescriptionTransform ?? optionsAttribute?.ValueDescriptionTransform - ?? NameTransform.None; - - int additionalPositionalArgumentCount = 0; - MemberInfo[] properties = _argumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance); - MethodInfo[] methods = _argumentsType.GetMethods(BindingFlags.Public | BindingFlags.Static); - foreach (var member in properties.Concat(methods)) + else { - if (Attribute.IsDefined(member, typeof(CommandLineArgumentAttribute))) + // If this is a multi-value argument is must be the last argument. + if (positionalArgumentIndex < _positionalArgumentCount && _arguments[positionalArgumentIndex].MultiValueInfo == null) { - var argument = member switch - { - PropertyInfo prop => CommandLineArgument.Create(this, prop), - MethodInfo method => CommandLineArgument.Create(this, method), - _ => throw new InvalidOperationException(), - }; - - AddNamedArgument(argument); - if (argument.Position != null) + // Skip named positional arguments that have already been specified by name. + while (positionalArgumentIndex < _positionalArgumentCount && _arguments[positionalArgumentIndex].MultiValueInfo == null && _arguments[positionalArgumentIndex].HasValue) { - ++additionalPositionalArgumentCount; + ++positionalArgumentIndex; } } - } - - return additionalPositionalArgumentCount; - } - - private void DetermineAutomaticArguments(ParseOptions? options, ParseOptionsAttribute? optionsAttribute) - { - var valueDescriptionTransform = options?.ValueDescriptionTransform ?? optionsAttribute?.ValueDescriptionTransform - ?? NameTransform.None; - - bool autoHelp = options?.AutoHelpArgument ?? optionsAttribute?.AutoHelpArgument ?? true; - if (autoHelp) - { - var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this, options?.DefaultValueDescriptions, - valueDescriptionTransform); - if (created) + if (positionalArgumentIndex >= _positionalArgumentCount) { - AddNamedArgument(argument); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); } - HelpArgument = argument; - } - - bool autoVersion = options?.AutoVersionArgument ?? optionsAttribute?.AutoVersionArgument ?? true; - if (autoVersion && !CommandInfo.IsCommand(_argumentsType)) - { - var argument = CommandLineArgument.CreateAutomaticVersion(this, options?.DefaultValueDescriptions, - valueDescriptionTransform); - - if (argument != null) + lastArgument = _arguments[positionalArgumentIndex]; + cancelParsing = ParseArgumentValue(lastArgument, arg, arg.AsMemory()); + if (cancelParsing != CancelMode.None) { - AddNamedArgument(argument); + break; } } } - private void AddNamedArgument(CommandLineArgument argument) + if (cancelParsing == CancelMode.Abort) { - if (argument.ArgumentName.Contains(NameValueSeparator)) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.ArgumentNameContainsSeparatorFormat, argument.ArgumentName)); - } + ParseResult = ParseResult.FromCanceled(lastArgument!.ArgumentName, args.Slice(x + 1)); + return null; + } - if (argument.HasLongName) - { - _argumentsByName.Add(argument.ArgumentName, argument); - if (argument.Aliases != null) - { - foreach (string alias in argument.Aliases) - { - _argumentsByName.Add(alias, argument); - } - } - } + // Check required arguments and post-parsing validation. This is done in usage order. + foreach (CommandLineArgument argument in _arguments) + { + argument.ValidateAfterParsing(); + } - if (_argumentsByShortName != null && argument.HasShortName) + // Run class validators. + _provider.RunValidators(this); + + object commandLineArguments; + try + { + object?[]? requiredPropertyValues = null; + if (_requiredPropertyArguments != null) { - _argumentsByShortName.Add(argument.ShortName.ToString(), argument); - if (argument.ShortAliases != null) + requiredPropertyValues = new object?[_requiredPropertyArguments.Count]; + for (int i = 0; i < requiredPropertyValues.Length; ++i) { - foreach (var alias in argument.ShortAliases) - { - _argumentsByShortName.Add(alias.ToString(), argument); - } + requiredPropertyValues[i] = _requiredPropertyArguments[i].Value; } } - _arguments.Add(argument); + commandLineArguments = _provider.CreateInstance(this, requiredPropertyValues); } - - private void VerifyPositionalArgumentRules() + catch (TargetInvocationException ex) { - bool hasOptionalArgument = false; - bool hasArrayArgument = false; - - for (int x = 0; x < _positionalArgumentCount; ++x) - { - CommandLineArgument argument = _arguments[x]; - - if (hasArrayArgument) - { - throw new NotSupportedException(Properties.Resources.ArrayNotLastArgument); - } - - if (argument.IsRequired && hasOptionalArgument) - { - throw new NotSupportedException(Properties.Resources.InvalidOptionalArgumentOrder); - } + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex.InnerException); + } + catch (Exception ex) + { + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex); + } - if (!argument.IsRequired) - { - hasOptionalArgument = true; - } + foreach (CommandLineArgument argument in _arguments) + { + // Apply property argument values (this does nothing for method arguments). + argument.ApplyPropertyValue(commandLineArguments); + } - if (argument.IsMultiValue) - { - hasArrayArgument = true; - } + ParseResult = cancelParsing == CancelMode.None + ? ParseResult.FromSuccess() + : ParseResult.FromSuccess(lastArgument!.ArgumentName, args.Slice(x + 1)); - argument.Position = x; - } - } + // Reset to false in case it was set by a method argument that didn't cancel parsing. + HelpRequested = false; + return commandLineArguments; + } - private object? ParseCore(string[] args, int index) + private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) + { + if (argument.HasValue && argument.MultiValueInfo == null) { - if (args == null) + if (!AllowDuplicateArguments) { - throw new ArgumentNullException(nameof(index)); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.DuplicateArgument, argument); } - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + var duplicateEventArgs = stringValue == null + ? new DuplicateArgumentEventArgs(argument, memoryValue.HasValue, memoryValue ?? default) + : new DuplicateArgumentEventArgs(argument, stringValue); - // Reset all arguments to their default value. - foreach (CommandLineArgument argument in _arguments) + OnDuplicateArgument(duplicateEventArgs); + if (duplicateEventArgs.KeepOldValue) { - argument.Reset(); + return CancelMode.None; } + } - HelpRequested = false; - int positionalArgumentIndex = 0; - - for (int x = index; x < args.Length; ++x) - { - string arg = args[x]; - var argumentNamePrefix = CheckArgumentNamePrefix(arg); - if (argumentNamePrefix != null) - { - // 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. - x = ParseNamedArgument(args, x, argumentNamePrefix.Value); - if (x < 0) - { - return null; - } - } - else - { - // If this is a multi-value argument is must be the last argument. - if (positionalArgumentIndex < _positionalArgumentCount && !_arguments[positionalArgumentIndex].IsMultiValue) - { - // Skip named positional arguments that have already been specified by name. - while (positionalArgumentIndex < _positionalArgumentCount && !_arguments[positionalArgumentIndex].IsMultiValue && _arguments[positionalArgumentIndex].HasValue) - { - ++positionalArgumentIndex; - } - } + var cancelParsing = argument.SetValue(Culture, memoryValue.HasValue, stringValue, (memoryValue ?? default).Span); + var e = new ArgumentParsedEventArgs(argument) + { + CancelParsing = cancelParsing + }; - if (positionalArgumentIndex >= _positionalArgumentCount) - { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); - } + if (argument.CancelParsing != CancelMode.None) + { + e.CancelParsing = argument.CancelParsing; + } - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler - // or the CancelParsing property. - if (ParseArgumentValue(_arguments[positionalArgumentIndex], arg)) - { - return null; - } - } - } + OnArgumentParsed(e); - // Check required arguments and post-parsing validation. This is done in usage order. - foreach (CommandLineArgument argument in _arguments) + if (e.CancelParsing != CancelMode.None) + { + // Automatically request help only if the cancellation was due to the + // CommandLineArgumentAttribute.CancelParsing property. + if (argument.CancelParsing == CancelMode.Abort) { - argument.ValidateAfterParsing(); + HelpRequested = true; } + } + + return e.CancelParsing; + } + + private (CancelMode, int, CommandLineArgument?) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) + { + var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitFirstOfAny(_nameValueSeparators.AsSpan()); - // Run class validators. - foreach (var validator in _argumentsType.GetCustomAttributes()) + CancelMode cancelParsing; + CommandLineArgument? argument = null; + if (_argumentsByShortName != null && prefix.Short) + { + if (argumentName.Length == 1) { - validator.Validate(this); + argument = GetShortArgumentOrThrow(argumentName.Span[0]); } - - var count = _constructorArgumentCount; - if (_injectionIndex >= 0) + else { - ++count; + CommandLineArgument? lastArgument; + (cancelParsing, lastArgument) = ParseShortArgument(argumentName.Span, argumentValue); + return (cancelParsing, index, lastArgument); } + } - var constructorArgumentValues = new object?[count]; - int offset = 0; - for (int x = 0; x < count; ++x) + if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) + { + if (Options.AutoPrefixAliasesOrDefault) { - if (x == _injectionIndex) - { - constructorArgumentValues[x] = this; - offset = 1; - } - else - { - constructorArgumentValues[x] = _arguments[x - offset].Value; - } + argument = GetArgumentByNamePrefix(argumentName.Span); } - object commandLineArguments = CreateArgumentsTypeInstance(constructorArgumentValues); - foreach (CommandLineArgument argument in _arguments) + if (argument == null) { - // Apply property argument values (this does nothing for constructor or method arguments). - argument.ApplyPropertyValue(commandLineArguments); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); } - - ParseResult = ParseResult.Success; - return commandLineArguments; } - private bool ParseArgumentValue(CommandLineArgument argument, string? value) + argument.SetUsedArgumentName(argumentName); + if (!argumentValue.HasValue && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) { - if (argument.HasValue && !argument.IsMultiValue) + 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 (!AllowDuplicateArguments) + ++index; + argumentValueString = args[index]; + + cancelParsing = ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory()); + if (cancelParsing != CancelMode.None) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.DuplicateArgument, argument); + return (cancelParsing, index, argument); } - var duplicateEventArgs = new DuplicateArgumentEventArgs(argument, value); - OnDuplicateArgument(duplicateEventArgs); - if (duplicateEventArgs.KeepOldValue) + if (argument.MultiValueInfo is not MultiValueArgumentInfo { AllowWhiteSpaceSeparator: true }) { - return false; + break; } } - bool continueParsing = argument.SetValue(Culture, value); - var e = new ArgumentParsedEventArgs(argument) - { - Cancel = !continueParsing - }; - - OnArgumentParsed(e); - var cancel = e.Cancel || (argument.CancelParsing && !e.OverrideCancelParsing); - - // Automatically request help only if the cancellation was not due to the SetValue - // call. - if (continueParsing) - { - HelpRequested = cancel; - } - - if (cancel) + if (argumentValueString != null) { - ParseResult = ParseResult.FromCanceled(argument.ArgumentName); + return (CancelMode.None, index, argument); } - - return cancel; } - private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) - { - var (argumentName, argumentValue) = args[index].SplitOnce(NameValueSeparator, prefix.Prefix.Length); - - CommandLineArgument? argument = null; - if (_argumentsByShortName != null && prefix.Short) - { - if (argumentName.Length == 1) - { - argument = GetShortArgumentOrThrow(argumentName); - } - else - { - // ParseShortArgument returns true if parsing was canceled by the - // ArgumentParsed event handler or the CancelParsing property. - return ParseShortArgument(argumentName, argumentValue) ? -1 : index; - } - } + // 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); + } - if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) + private CommandLineArgument? GetArgumentByNamePrefix(ReadOnlySpan prefix) + { + CommandLineArgument? foundArgument = null; + foreach (var argument in _arguments) + { + // Skip arguments without a long name. + if (Mode == ParsingMode.LongShort && !argument.HasLongName) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName); + continue; } - argument.UsedArgumentName = argumentName; - if (argumentValue == null && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) + var matches = argument.ArgumentName.AsSpan().StartsWith(prefix, ArgumentNameComparison); + if (!matches) { - // 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) + foreach (var alias in argument.Aliases) { - ++index; - argumentValue = args[index]; - - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed - // event handler or the CancelParsing property. - if (ParseArgumentValue(argument, argumentValue)) - { - return -1; - } - - if (!argument.AllowMultiValueWhiteSpaceSeparator) + if (alias.AsSpan().StartsWith(prefix, ArgumentNameComparison)) { + matches = true; break; } } - - if (argumentValue != null) - { - return index; - } } - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler - // or the CancelParsing property. - return ParseArgumentValue(argument, argumentValue) ? -1 : index; - } - - private bool ParseShortArgument(string name, string? value) - { - foreach (var ch in name) + if (matches) { - var arg = GetShortArgumentOrThrow(ch.ToString()); - if (!arg.IsSwitch) + if (foundArgument != null) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name); + // Prefix is not unique. + return null; } - if (ParseArgumentValue(arg, value)) - { - return true; - } + foundArgument = argument; } - - return false; } - private CommandLineArgument GetShortArgumentOrThrow(string shortName) + return foundArgument; + } + + private (CancelMode, CommandLineArgument?) ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) + { + CommandLineArgument? arg = null; + foreach (var ch in name) { - Debug.Assert(shortName.Length == 1); - if (_argumentsByShortName!.TryGetValue(shortName, out CommandLineArgument? argument)) + arg = GetShortArgumentOrThrow(ch); + if (!arg.IsSwitch) { - return argument; + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); } - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, shortName); + var cancelParsing = ParseArgumentValue(arg, null, value); + if (cancelParsing != CancelMode.None) + { + return (cancelParsing, arg); + } } - private PrefixInfo? CheckArgumentNamePrefix(string argument) + return (CancelMode.None, arg); + } + + private CommandLineArgument GetShortArgumentOrThrow(char shortName) + { + if (_argumentsByShortName!.TryGetValue(shortName, out CommandLineArgument? 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. - if (argument.Length >= 2 && argument[0] == '-' && char.IsDigit(argument, 1)) - { - return null; - } + return argument; + } - foreach (var prefix in _sortedPrefixes) - { - if (argument.StartsWith(prefix.Prefix, StringComparison.Ordinal)) - { - return prefix; - } - } + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, shortName.ToString()); + } + 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. + if (argument.Length >= 2 && argument[0] == '-' && char.IsDigit(argument, 1)) + { return null; } - private ConstructorInfo GetCommandLineConstructor() + foreach (var prefix in _sortedPrefixes) { - ConstructorInfo[] ctors = _argumentsType.GetConstructors(); - if (ctors.Length < 1) + if (argument.StartsWith(prefix.Prefix, StringComparison.Ordinal)) { - throw new NotSupportedException(Properties.Resources.NoConstructor); + return prefix; } - else if (ctors.Length == 1) - { - return ctors[0]; - } - - var markedCtors = ctors.Where(c => Attribute.IsDefined(c, typeof(CommandLineConstructorAttribute))); - if (!markedCtors.Any()) - { - throw new NotSupportedException(Properties.Resources.NoMarkedConstructor); - } - else if (markedCtors.Count() > 1) - { - throw new NotSupportedException(Properties.Resources.MultipleMarkedConstructors); - } - - return markedCtors.First(); } - private object CreateArgumentsTypeInstance(object?[] constructorArgumentValues) + return null; + } + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] +#endif + private static ArgumentProvider GetArgumentProvider(Type type, ParseOptions? options) + { + // Try to use the generated provider if it exists. + var forceReflection = options?.ForceReflection ?? ParseOptions.ForceReflectionDefault; + if (!forceReflection && Attribute.IsDefined(type, typeof(GeneratedParserAttribute))) { - try - { - return _commandLineConstructor.Invoke(constructorArgumentValues); - } - catch (TargetInvocationException ex) + var providerType = type.GetNestedType("OokiiCommandLineArgumentProvider", BindingFlags.NonPublic); + if (providerType != null && typeof(ArgumentProvider).IsAssignableFrom(providerType)) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex.InnerException); + return (ArgumentProvider)Activator.CreateInstance(providerType)!; } } + + return new ReflectionArgumentProvider(type); } } diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index edea07e5..6836bfe8 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -1,69 +1,130 @@ -using System; +using Ookii.CommandLine.Support; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// A generic version of the class that offers strongly typed +/// and methods. +/// +/// The type that defines the arguments. +/// +/// +/// This class provides the same functionality as the class. +/// The only difference is that the method, the +/// method, and their overloads return the arguments type, which avoids the need to cast at the +/// call site. +/// +/// +/// +public class CommandLineParser : CommandLineParser + where T : class { /// - /// A generic version of the class that offers strongly typed - /// methods. + /// Initializes a new instance of the class using the + /// specified options. /// - /// The type that defines the arguments. + /// + /// + /// + /// + /// The cannot use type as the + /// command line arguments type, because it violates one of the rules concerning argument + /// names or positions, or has an argument type that cannot be parsed. + /// /// /// - /// This class provides the same functionality as the class. - /// The only difference is that the method and overloads return the - /// correct type, which avoids casting. + /// This constructor uses reflection to determine the arguments defined by the type + /// t runtime, unless the type has the applied. For a + /// type using that attribute, you can also use the generated static + /// or methods on the + /// arguments class instead. + /// + /// + /// If the parameter is not , the + /// instance passed in will be modified to reflect the options from the arguments class's + /// attribute, if it has one. /// /// - /// If you don't intend to manually handle errors and usage help printing, and don't need - /// to inspect the state of the instance, the static - /// should be used instead. + /// Certain properties of the class can be changed after the + /// class has been constructed, and still affect the + /// parsing behavior. See the property for details. /// /// - public class CommandLineParser : CommandLineParser - where T : class +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] +#endif + public CommandLineParser(ParseOptions? options = null) + : base(typeof(T), options) { - /// - /// Initializes a new instance of the class using the - /// specified options. - /// - /// - /// - /// - /// - /// The cannot use type as the - /// command line arguments type, because it violates one of the rules concerning argument - /// names or positions, or has an argument type that cannot be parsed. - /// - /// - /// - /// - public CommandLineParser(ParseOptions? options = null) - : base(typeof(T), options) - { - } + } - /// - public new T? Parse() + /// + /// Initializes a new instance of the class using the + /// specified argument provider and options. + /// + /// + /// + /// + /// + /// + /// + /// + /// The cannot use type as the + /// command line arguments type, because it violates one of the rules concerning argument + /// names or positions, or has an argument type that cannot be parsed. + /// + /// + /// The property for the + /// if a different type than . + /// + /// + /// + /// + public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null) + : base(provider, options) + { + if (provider.ArgumentsType != typeof(T)) { - return (T?)base.Parse(); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.IncorrectProviderTypeFormat, typeof(T)), nameof(provider)); } + } - /// - public new T? Parse(string[] args, int index = 0) - { - return (T?)base.Parse(args, index); - } + /// + public new T? Parse() + { + return (T?)base.Parse(); + } - /// - public new T? ParseWithErrorHandling() - { - return (T?)base.ParseWithErrorHandling(); - } + /// + public new T? Parse(string[] args) + { + return (T?)base.Parse(args); + } - /// - public new T? ParseWithErrorHandling(string[] args, int index = 0) - { - return (T?)base.ParseWithErrorHandling(args, index); - } + /// + public new T? Parse(ReadOnlyMemory args) + { + return (T?)base.Parse(args); + } + + /// + public new T? ParseWithErrorHandling() + { + return (T?)base.ParseWithErrorHandling(); + } + + /// + public new T? ParseWithErrorHandling(string[] args) + { + return (T?)base.ParseWithErrorHandling(args); + } + + /// + public new T? ParseWithErrorHandling(ReadOnlyMemory args) + { + return (T?)base.ParseWithErrorHandling(args); } } diff --git a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs index b5aa13b9..89aa9f5c 100644 --- a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs +++ b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs @@ -1,23 +1,29 @@ using System.Threading.Tasks; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Base class for asynchronous commands that want the method to +/// invoke the method. +/// +/// +/// +/// This class is provided for convenience for creating asynchronous commands without having to +/// implement the method. +/// +/// +/// +public abstract class AsyncCommandBase : IAsyncCommand { /// - /// Base class for asynchronous tasks that want the method to - /// invoke the method. + /// Calls the method and waits synchronously for it to complete. /// - public abstract class AsyncCommandBase : IAsyncCommand + /// The exit code of the command. + public virtual int Run() { - /// - /// Calls the method and waits synchronously for it to complete. - /// - /// The exit code of the command. - public virtual int Run() - { - return Task.Run(RunAsync).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - public abstract Task RunAsync(); + return Task.Run(RunAsync).ConfigureAwait(false).GetAwaiter().GetResult(); } + + /// + public abstract Task RunAsync(); } diff --git a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs index c19d92eb..b258e1d0 100644 --- a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs @@ -1,33 +1,59 @@ -using System; +using Ookii.CommandLine.Support; +using System; +using System.Collections.Generic; using System.Reflection; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +[Command] +internal class AutomaticVersionCommand : ICommand { - [Command] - internal class AutomaticVersionCommand : ICommand + private class ArgumentProvider : GeneratedArgumentProvider { - private readonly CommandLineParser _parser; + private readonly LocalizedStringProvider _stringProvider; - public AutomaticVersionCommand(CommandLineParser parser) + public ArgumentProvider(LocalizedStringProvider stringProvider) + : base(typeof(AutomaticVersionCommand), null, null, null, null) { - _parser = parser; + _stringProvider = stringProvider; } - public int Run() + public override bool IsCommand => true; + + public override string Description => _stringProvider.AutomaticVersionCommandDescription(); + + public override object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues) => new AutomaticVersionCommand(parser); + + public override IEnumerable GetArguments(CommandLineParser parser) { - var assembly = Assembly.GetEntryAssembly(); - if (assembly == null) - { - Console.WriteLine(Properties.Resources.UnknownVersion); - return 1; - } - - // We can't use _parser.ApplicationFriendlyName because we're interested in the entry - // assembly, not the one containing this command. - var attribute = assembly.GetCustomAttribute(); - var friendlyName = attribute?.Name ?? assembly.GetName().Name ?? string.Empty; - CommandLineArgument.ShowVersion(_parser.StringProvider, assembly, friendlyName); - return 0; + yield break; } } + + private readonly CommandLineParser _parser; + + public AutomaticVersionCommand(CommandLineParser parser) + { + _parser = parser; + } + + public int Run() + { + var assembly = Assembly.GetEntryAssembly(); + if (assembly == null) + { + Console.WriteLine(Properties.Resources.UnknownVersion); + return 1; + } + + // We can't use _parser.ApplicationFriendlyName because we're interested in the entry + // assembly, not the one containing this command. + var attribute = assembly.GetCustomAttribute(); + var friendlyName = attribute?.Name ?? assembly.GetName().Name ?? string.Empty; + CommandLineArgument.ShowVersion(_parser.StringProvider, assembly, friendlyName); + return 0; + } + + public static CommandLineParser CreateParser(ParseOptions options) + => new(new ArgumentProvider(options.StringProvider), options); } diff --git a/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs b/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs new file mode 100644 index 00000000..040aecca --- /dev/null +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ookii.CommandLine.Commands; + +internal class AutomaticVersionCommandInfo : CommandInfo +{ + public AutomaticVersionCommandInfo(CommandManager manager) + : base(typeof(AutomaticVersionCommand), manager.Options.AutoVersionCommandName(), manager) + { + } + + public override string? Description => Manager.Options.StringProvider.AutomaticVersionCommandDescription(); + + public override bool UseCustomArgumentParsing => false; + + public override IEnumerable Aliases => Enumerable.Empty(); + + public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() + => throw new InvalidOperationException(Properties.Resources.NoCustomParsing); + + public override CommandLineParser CreateParser() + => AutomaticVersionCommand.CreateParser(Manager.Options); +} diff --git a/src/Ookii.CommandLine/Commands/CommandAttribute.cs b/src/Ookii.CommandLine/Commands/CommandAttribute.cs index 424cebc7..58300004 100644 --- a/src/Ookii.CommandLine/Commands/CommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/CommandAttribute.cs @@ -1,86 +1,86 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Attribute that indicates a class implementing the interface is a +/// subcommand. +/// +/// +/// +/// To be considered a subcommand, a class must both implement the +/// interface and have the attribute applied. This allows classes +/// that implement the interface, but do not have the attribute, to be used +/// as common base classes for other commands, without being commands themselves. +/// +/// +/// If a command does not have an explicit name, its name is determined by taking the type name +/// of the command class and applying the transformation specified by the +/// property. +/// +/// +/// Alternative names for a command can be given by using the +/// attribute. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class CommandAttribute : Attribute { + private readonly string? _commandName; + /// - /// Attribute that indicates a class implementing the interface is a - /// subcommand. + /// Initializes a new instance of the class using the target's + /// type name as the command name. /// /// /// - /// To be considered a subcommand, a class must both implement the - /// interface and have the applied. - /// - /// - /// This allows classes implementing but without the attribute to be - /// used as common base classes for other commands, without being commands themselves. - /// - /// - /// If a command has no explicit name, its name is determined by taking the type name - /// and applying the transformation specified by the + /// If a command does not have an explicit name, its name is determined by taking the type name + /// and applying the transformation specified by the /// property. /// - /// - /// A command can be given more than one name by using the - /// attribute. - /// /// - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class CommandAttribute : Attribute + public CommandAttribute() { - private readonly string? _commandName; - - /// - /// Initializes a new instance of the class using the target's - /// type name as the command name. - /// - /// - /// - /// If a command has no explicit name, its name is determined by taking the type name - /// and applying the transformation specified by the - /// property. - /// - /// - public CommandAttribute() - { - } + } - /// - /// Initializes a new instance of the class using the specified command name. - /// - /// The name of the command, which can be used to locate it using the method. - /// - /// is . - /// - public CommandAttribute(string commandName) - { - _commandName = commandName ?? throw new ArgumentNullException(nameof(commandName)); - } + /// + /// Initializes a new instance of the class using the specified command name. + /// + /// + /// The name of the command, which can be used to invoke the command or to retrieve it using the + /// method. + /// + /// + /// is . + /// + public CommandAttribute(string commandName) + { + _commandName = commandName ?? throw new ArgumentNullException(nameof(commandName)); + } - /// - /// Gets the name of the command, which can be used to locate it using the method. - /// - /// - /// The name of the command, or to use the type name as the command - /// name. - /// - public string? CommandName => _commandName; + /// + /// Gets the name of the command, which can be used to invoke the command or to retrieve it + /// using the method. + /// + /// + /// The name of the command, or if the target type name will be used as + /// the name. + /// + public string? CommandName => _commandName; - /// - /// Gets or sets a value that indicates whether the command is hidden from the usage help. - /// - /// - /// if the command is hidden from the usage help; otherwise, - /// . The default value is . - /// - /// - /// - /// A hidden command will not be included in the command list when usage help is - /// displayed, but can still be invoked from the command line. - /// - /// - public bool IsHidden { get; set; } - } + /// + /// Gets or sets a value that indicates whether the command is hidden from the usage help. + /// + /// + /// if the command is hidden from the usage help; otherwise, + /// . The default value is . + /// + /// + /// + /// A hidden command will not be included in the command list when usage help is + /// displayed, but can still be invoked from the command line. + /// + /// + public bool IsHidden { get; set; } } diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 4f0cfa94..626ae179 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -1,336 +1,403 @@ -using System; +using Ookii.CommandLine.Support; +using System; using System.Collections.Generic; using System.ComponentModel; -using System.Globalization; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Provides information about a subcommand. +/// +/// +/// +/// +/// +public abstract class CommandInfo { + private readonly CommandManager _manager; + private readonly string _name; + private readonly Type _commandType; + private readonly CommandAttribute _attribute; + /// - /// Provides information about a subcommand. + /// Initializes a new instance of the class. /// - /// - /// - /// - public struct CommandInfo + /// The type that implements the subcommand. + /// The for the subcommand type. + /// The of a command that is the parent of this command. + /// + /// The that is managing this command. + /// + /// + /// or is . + /// + protected CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager, Type? parentCommandType) { - private readonly CommandManager _manager; - private readonly string _name; - private readonly Type _commandType; - private readonly CommandAttribute _attribute; - private string? _description; + _manager = manager ?? throw new ArgumentNullException(nameof(manager)); + _name = GetName(attribute, commandType, manager.Options); + _commandType = commandType; + _attribute = attribute; + ParentCommandType = parentCommandType; + } - /// - /// Initializes a new instance of the structure. - /// - /// The type that implements the subcommand. - /// - /// The that is managing this command. - /// - /// - /// or is . - /// - /// - /// is not a command type. - /// - public CommandInfo(Type commandType, CommandManager manager) - : this(commandType, GetCommandAttributeOrThrow(commandType), manager) - { - } + internal CommandInfo(Type commandType, string name, CommandManager manager) + { + _manager = manager; + _name = name; + _commandType = commandType; + _attribute = new(); + } - private CommandInfo(string name, Type commandType, string description, CommandManager manager) - { - _manager = manager; - _attribute = GetCommandAttribute(commandType)!; - _name = name; - _commandType = commandType; - _description = description; - } + /// + /// Gets the that this instance belongs to. + /// + /// + /// An instance of the class. + /// + public CommandManager Manager => _manager; - private CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager) - { - _manager = manager ?? throw new ArgumentNullException(nameof(manager)); - _name = GetName(attribute, commandType, manager.Options); - _commandType = commandType; - _description = null; - _attribute = attribute; - } + /// + /// Gets the name of the command. + /// + /// + /// The name of the command. + /// + /// + /// + /// The name is taken from the property. If + /// that property is , the name is determined by taking the command + /// type's name, and applying the transformation specified by the + /// property. + /// + /// + public string Name => _name; - /// - /// Gets the name of the command. - /// - /// - /// The name of the command. - /// - /// - /// - /// The name is taken from the property. If - /// that property is , the name is determined by taking the command - /// type's name, and applying the transformation specified by the - /// property. - /// - /// - public string Name => _name; + /// + /// Gets the type that implements the command. + /// + /// + /// The type that implements the command. + /// + public Type CommandType => _commandType; - /// - /// Gets the type that implements the command. - /// - /// - /// The type that implements the command. - /// - public Type CommandType => _commandType; + /// + /// Gets the description of the command. + /// + /// + /// The description of the command, determined using the + /// attribute. + /// + public abstract string? Description { get; } - /// - /// Gets the description of the command. - /// - /// - /// The description of the command, determined using the - /// attribute. - /// - public string? Description => _description ??= GetCommandDescription(); + /// + /// Gets a value that indicates if the command uses custom parsing. + /// + /// + /// if the command type implements the + /// interface; otherwise, . + /// + public abstract bool UseCustomArgumentParsing { get; } - /// - /// Gets a value that indicates if the command uses custom parsing. - /// - /// - /// if the command type implements the - /// interface; otherwise, . - /// - public bool UseCustomArgumentParsing => _commandType.ImplementsInterface(typeof(ICommandWithCustomParsing)); + /// + /// Gets or sets a value that indicates whether the command is hidden from the usage help. + /// + /// + /// if the command is hidden from the usage help; otherwise, + /// . + /// + /// + /// + /// A hidden command will not be included in the command list when usage help is + /// displayed, but can still be invoked from the command line. + /// + /// + /// + public bool IsHidden => _attribute.IsHidden; - /// - /// Gets or sets a value that indicates whether the command is hidden from the usage help. - /// - /// - /// if the command is hidden from the usage help; otherwise, - /// . - /// - /// - /// - /// A hidden command will not be included in the command list when usage help is - /// displayed, but can still be invoked from the command line. - /// - /// - /// - public bool IsHidden => _attribute.IsHidden; + /// + /// Gets the alternative names of this command. + /// + /// + /// A list of aliases. + /// + /// + /// + /// Aliases for a command are specified by using the attribute + /// on a class implementing the interface. + /// + /// + public abstract IEnumerable Aliases { get; } - /// - /// Gets the alternative names of this command. - /// - /// - /// A list of aliases. - /// - /// - /// - /// Aliases for a command are specified by using the on a - /// class implementing the interface. - /// - /// - public IEnumerable Aliases => _commandType.GetCustomAttributes().Select(a => a.Alias); + /// + /// Gets the type of the command that is the parent of this command. + /// + /// + /// The of the parent command, or if this command + /// does not have a parent. + /// + /// + /// + /// Subcommands can specify their parent using the + /// attribute. + /// + /// + /// The class will only use commands whose parent command + /// type matches the value of the property. + /// + /// + public Type? ParentCommandType { get; } - /// - /// Creates an instance of the command type. - /// - /// The arguments to the command. - /// The index in at which to start parsing the arguments. - /// - /// An instance of the , or if an error - /// occurred or parsing was canceled. - /// - /// - /// is . - /// - /// does not fall inside the bounds of . - public ICommand? CreateInstance(string[] args, int index) - { - var (command, _) = CreateInstanceWithResult(args, index); - return command; - } + /// + /// Creates an instance of the command type parsing the specified arguments. + /// + /// The arguments to the command. + /// + /// An instance of the , or if an error + /// occurred or parsing was canceled. + /// + /// + /// + /// If the type indicated by the property implements the + /// parsing interface, an instance of the type is + /// created and the method + /// invoked. Otherwise, an instance of the type is created using the + /// class. + /// + /// + public ICommand? CreateInstance(ReadOnlyMemory args) + { + var (command, _) = CreateInstanceWithResult(args); + return command; + } - /// - /// Creates an instance of the command type. - /// - /// The arguments to the command. - /// The index in at which to start parsing the arguments. - /// - /// A tuple containing an instance of the , or if an error - /// occurred or parsing was canceled, and the of the operation. - /// - /// - /// - /// The property of the returned - /// will be if the command used custom parsing. - /// - /// - /// - /// is . - /// - /// does not fall inside the bounds of . - public (ICommand?, ParseResult) CreateInstanceWithResult(string[] args, int index) + /// + /// Creates an instance of the command type by parsing the specified arguments, and returns it + /// in addition to the result of the parsing operation. + /// + /// The arguments to the command. + /// + /// A tuple containing an instance of the , or if an error + /// occurred or parsing was canceled, and the of the operation. + /// + /// + /// + /// If the type indicated by the property implements the + /// parsing interface, an instance of the type is + /// created and the method + /// invoked. Otherwise, an instance of the type is created using the + /// class. + /// + /// + /// The property of the returned + /// will be if the command used custom parsing. + /// + /// + public (ICommand?, ParseResult) CreateInstanceWithResult(ReadOnlyMemory args) + { + if (UseCustomArgumentParsing) { - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (UseCustomArgumentParsing) - { - var command = (ICommandWithCustomParsing)Activator.CreateInstance(CommandType)!; - command.Parse(args, index, _manager.Options); - return (command, default); - } - else - { - var parser = CreateParser(); - var command = (ICommand?)parser.ParseWithErrorHandling(args, index); - return (command, parser.ParseResult); - } + var command = CreateInstanceWithCustomParsing(); + command.Parse(args, _manager); + return (command, default); } - - /// - /// Creates a instance that can be used to instantiate - /// - /// - /// A instance for the . - /// - /// - /// The command uses the interface. - /// - /// - /// - /// If the property is , the - /// command cannot be created suing the class, and you - /// must use the method. - /// - /// - public CommandLineParser CreateParser() + else { - if (UseCustomArgumentParsing) - { - throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); - } - - return new CommandLineParser(CommandType, _manager.Options); + var parser = CreateParser(); + var command = (ICommand?)parser.ParseWithErrorHandling(args); + return (command, parser.ParseResult); } + } - /// - /// Checks whether the command's name or aliases match the specified name. - /// - /// The name to check for. - /// - /// The to use for the comparisons, or - /// to use the default comparison, which is . - /// - /// - /// if the matches the - /// property or any of the items in the property. - /// - /// - /// is . - /// - public bool MatchesName(string name, IComparer? comparer = null) - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + /// + /// Creates a instance for the type indicated by the + /// property. + /// + /// + /// A instance for the . + /// + /// + /// The command uses the interface. + /// + /// + /// + /// If the property is , the + /// command cannot be created using the class, and you + /// must use the method or + /// method instead. + /// + /// + public abstract CommandLineParser CreateParser(); - comparer ??= StringComparer.OrdinalIgnoreCase; - if (comparer.Compare(name, _name) == 0) - { - return true; - } + /// + /// Creates an instance of a command that uses the + /// interface. + /// + /// An instance of the command type. + /// + /// The command does not use the interface. + /// + /// + /// + /// It is the responsibility of the caller to invoke the + /// method after the instance is created. + /// + /// + public abstract ICommandWithCustomParsing CreateInstanceWithCustomParsing(); - return Aliases.Any(alias => comparer.Compare(name, alias) == 0); + /// + /// Checks whether the command's name or aliases match the specified name. + /// + /// The name to check for. + /// + /// if the matches the + /// property or any of the items in the property. + /// + /// + /// + /// Automatic prefix aliases are not considered by this method, regardless of the value of + /// the property. To check for a prefix, + /// use the method. + /// + /// + /// + /// is . + /// + public bool MatchesName(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); } - /// - /// Creates an instance of the structure only if - /// represents a command type. - /// - /// The type that implements the subcommand. - /// - /// The that is managing this command. - /// - /// - /// or is . - /// - /// - /// A structure with information about the command, or - /// if was not a command. - /// - public static CommandInfo? TryCreate(Type commandType, CommandManager manager) + if (string.Equals(name, _name, Manager.Options.CommandNameComparison)) { - var attribute = GetCommandAttribute(commandType); - if (attribute == null) - { - return null; - } - - return new CommandInfo(commandType, attribute, manager); + return true; } - /// - /// Returns a value indicating if the specified type is a subcommand. - /// - /// The type that implements the subcommand. - /// - /// if the type implements the interface and - /// has the applied; otherwise, . - /// - /// - /// is . - /// - public static bool IsCommand(Type commandType) + return Aliases.Any(alias => string.Equals(name, alias, Manager.Options.CommandNameComparison)); + } + + /// + /// Checks whether the command's name or one of its aliases start with the specified prefix. + /// + /// The prefix to check for. + /// + /// if is a prefix of the + /// property or any of the items in the property. + /// + /// + /// is . + /// + public bool MatchesPrefix(string prefix) + { + if (prefix == null) { - return GetCommandAttribute(commandType) != null; + throw new ArgumentNullException(nameof(prefix)); } - internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) + if (Name.StartsWith(prefix, Manager.Options.CommandNameComparison)) { - var name = manager.Options.AutoVersionCommandName(); - var description = manager.Options.StringProvider.AutomaticVersionCommandDescription(); - return new CommandInfo(name, typeof(AutomaticVersionCommand), description, manager); + return true; } - private static CommandAttribute? GetCommandAttribute(Type commandType) - { - if (commandType == null) - { - throw new ArgumentNullException(nameof(commandType)); - } + return Aliases.Any(alias => alias.StartsWith(prefix, Manager.Options.CommandNameComparison)); + } - if (commandType.IsAbstract || !commandType.ImplementsInterface(typeof(ICommand))) - { - return null; - } + /// + /// Creates an instance of the class only if + /// represents a command type. + /// + /// The type that implements the subcommand. + /// + /// The that is managing this command. + /// + /// + /// or is . + /// + /// + /// If the type specified by implements the + /// interface, has the attribute, and is not , + /// a class with information about the command; otherwise, + /// . + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + public static CommandInfo? TryCreate(Type commandType, CommandManager manager) + => ReflectionCommandInfo.TryCreate(commandType, manager); - return commandType.GetCustomAttribute(); - } + /// + /// Creates an instance of the class for the specified command + /// type. + /// + /// The type that implements the subcommand. + /// + /// The that is managing this command. + /// + /// + /// or is . + /// + /// + /// is does not implement the interface, + /// does not have the attribute, or is . + /// + /// + /// A class with information about the command. + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + public static CommandInfo Create(Type commandType, CommandManager manager) + => new ReflectionCommandInfo(commandType, null, manager); - private static CommandAttribute GetCommandAttributeOrThrow(Type commandType) - { - return GetCommandAttribute(commandType) ?? - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, - Properties.Resources.TypeIsNotCommandFormat, commandType.FullName)); - } + /// + /// Returns a value indicating if the specified type is a subcommand. + /// + /// The type that implements the subcommand. + /// + /// if the type implements the interface, has the + /// attribute applied, and is not ; + /// otherwise, . + /// + /// + /// is . + /// + public static bool IsCommand( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type commandType + ) => GetCommandAttribute(commandType) != null; - private static string GetName(CommandAttribute attribute, Type commandType, CommandOptions? options) + internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) + => new AutomaticVersionCommandInfo(manager); + + internal static CommandAttribute? GetCommandAttribute( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type commandType) + { + if (commandType == null) { - return attribute.CommandName ?? - options?.CommandNameTransform.Apply(commandType.Name, options.StripCommandNameSuffix) ?? - commandType.Name; + throw new ArgumentNullException(nameof(commandType)); } - private string? GetCommandDescription() + if (commandType.IsAbstract || !commandType.ImplementsInterface(typeof(ICommand))) { - return _commandType.GetCustomAttribute()?.Description; + return null; } + + return commandType.GetCustomAttribute(); + } + + private static string GetName(CommandAttribute attribute, Type commandType, CommandOptions? options) + { + return attribute.CommandName ?? + options?.CommandNameTransform.Apply(commandType.Name, options.StripCommandNameSuffix) ?? + commandType.Name; } } diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index f976c61c..f56d8d18 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -1,566 +1,1049 @@ -using System; +using Ookii.CommandLine.Support; +using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Provides functionality to find and instantiate subcommands. +/// +/// +/// +/// Subcommands can be used to create applications that perform more than one operation, +/// where each operation has its own set of command line arguments. For example, think of +/// the dotnet executable, which has subcommands such as dotnet build and +/// dotnet run. +/// +/// +/// For a program using subcommands, typically the first command line argument will be the +/// name of the command, while the remaining arguments are arguments to the command. The +/// class provides functionality that makes creating an +/// application like this easy. +/// +/// +/// A subcommand is created by creating a class that implements the +/// interface, and applying the attribute to it. Implement +/// the method to implement the command's functionality. +/// +/// +/// Subcommand classes are instantiated using the class, and +/// follow the same rules as command line arguments classes, unless they implement the +/// interface. +/// +/// +/// Commands can be defined in a single assembly, or in multiple assemblies. +/// +/// +/// If you reuse the same instance or +/// instance to create multiple commands, the of one +/// command may affect the behavior of another. +/// +/// +/// +/// Usage documentation +/// +public class CommandManager { + private readonly CommandProvider _provider; + private readonly CommandOptions _options; + /// - /// Provides functionality to find and instantiate subcommands. + /// Initializes a new instance of the class for the assembly that + /// is calling the constructor. /// + /// + /// The options to use for parsing and usage help, or to use + /// the default options. + /// /// /// - /// Subcommands can be used to create applications that perform more than one operation, - /// where each operation has its own set of command line arguments. For example, think of - /// the dotnet executable, which has subcommands such as dotnet build and - /// dotnet run. + /// The class will look in the calling assembly for any public + /// or internal classes that implement the interface, have the + /// attribute, and are not . /// /// - /// For a program using subcommands, typically the first command line argument will be the - /// name of the command, while the remaining arguments are arguments to the command. The - /// class provides functionality that makes creating an - /// application like this easy. + /// This constructor uses reflection to determine which commands are available at runtime. To + /// use source generation to locate commands at compile time, use the + /// attribute. /// + /// + /// Once a command is created, the instance may be modified + /// with the options of the attribute applied to the + /// command class. Be aware of this if reusing the same or + /// instance to create multiple commands. + /// + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + public CommandManager(CommandOptions? options = null) + : this(new ReflectionCommandProvider(Assembly.GetCallingAssembly(), Assembly.GetCallingAssembly()), options) + { + } + + + /// + /// Initializes a new instance of the class using the + /// specified . + /// + /// + /// The that determines which commands are available. + /// + /// + /// The options to use for parsing and usage help, or to use + /// the default options. + /// + /// + /// + /// This constructor supports source generation, and should not typically be used directly + /// by application code. + /// + /// + /// Once a command is created, the instance may be modified + /// with the options of the attribute applied to the + /// command class. Be aware of this if reusing the same or + /// instance to create multiple commands. + /// + /// + /// + protected CommandManager(CommandProvider provider, CommandOptions? options = null) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _options = options ?? new(); + } + + /// + /// Initializes a new instance of the class using the specified + /// assembly. + /// + /// The assembly containing the commands. + /// + /// The options to use for parsing and usage help, or to use + /// the default options. + /// + /// + /// is . + /// + /// /// - /// A subcommand is created by creating a class that implements the - /// interface, and applying the attribute to it. Implement - /// the method to implement the command's functionality. + /// The class will look in the specified assembly for any public + /// classes that implement the interface, have the + /// attribute, and are not . /// /// - /// Subcommands classes are instantiated using the , and follow - /// the same rules as command line arguments classes. They can define command line arguments - /// using the properties and constructor parameters, which will be the arguments for the - /// command. + /// If is the assembly that called this constructor, both public + /// and internal command classes will be used. Otherwise, only public command classes are + /// used. /// /// - /// Commands can be defined in a single assembly, or multiple assemblies. + /// This constructor uses reflection to determine which commands are available at runtime. To + /// use source generation to locate commands at compile time, use the + /// attribute. /// /// - /// If you reuse the same instance or - /// instance to create multiple commands, the of one - /// command may affect the behavior of another. + /// Once a command is created, the instance may be modified + /// with the options of the attribute applied to the + /// command class. Be aware of this if reusing the same or + /// instance to create multiple commands. /// /// - /// - /// Usage documentation - public class CommandManager +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + public CommandManager(Assembly assembly, CommandOptions? options = null) + : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly)), Assembly.GetCallingAssembly()), options) { - private readonly Assembly? _assembly; - private readonly IEnumerable? _assemblies; - private readonly CommandOptions _options; - - /// - /// Initializes a new instance of the class for the calling - /// assembly. - /// - /// - /// The options to use for parsing and usage help, or to use - /// the default options. - /// - public CommandManager(CommandOptions? options = null) - : this(Assembly.GetCallingAssembly(), options) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The assembly containing the commands. - /// - /// The options to use for parsing and usage help, or to use - /// the default options. - /// - /// - /// is . - /// - /// - /// - /// Once a command is created, the instance may be modified - /// with the options of the attribute applied to the - /// command class. Be aware of this if reusing the same or - /// instance to create multiple commands. - /// - /// - public CommandManager(Assembly assembly, CommandOptions? options = null) - { - _assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); - _options = options ?? new(); - } + /// + /// Initializes a new instance of the class using the specified + /// assemblies. + /// + /// The assemblies containing the commands. + /// + /// The options to use for parsing and usage help, or to use + /// the default options. + /// + /// + /// or one of its elements is . + /// + /// + /// + /// The class will look in the specified assemblies for any + /// public classes that implement the interface, have the + /// attribute, and are not . + /// + /// + /// If an assembly in is the assembly that called this + /// constructor, both public and internal command classes will be used. For other assemblies, + /// only public classes are used. + /// + /// + /// This constructor uses reflection to determine which commands are available at runtime. To + /// use source generation to locate commands at compile time, use the + /// attribute. + /// + /// + /// Once a command is created, the instance may be modified + /// with the options of the attribute applied to the + /// command class. Be aware of this if reusing the same or + /// instance to create multiple commands. + /// + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + public CommandManager(IEnumerable assemblies, CommandOptions? options = null) + : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies)), Assembly.GetCallingAssembly()), options) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The assemblies containing the commands. - /// - /// The options to use for parsing and usage help, or to use - /// the default options. - /// - /// - /// or one of its elements is . - /// - public CommandManager(IEnumerable assemblies, CommandOptions? options = null) - { - _assemblies = assemblies ?? throw new ArgumentNullException(nameof(assemblies)); - _options = options ?? new(); + /// + /// Gets the options used by this instance. + /// + /// + /// An instance of the class. + /// + /// + /// + /// Modifying the options will change the way this instance behaves. + /// + /// + /// Once a command is created, the instance may be modified + /// with the options of the attribute applied to the + /// command class. Be aware of this if reusing the same or + /// instance to create multiple commands. + /// + /// + public CommandOptions Options => _options; - if (_assemblies.Any(a => a == null)) - { - throw new ArgumentNullException(nameof(assemblies)); - } - } + /// + /// Gets the result of parsing the arguments for the last call to . + /// + /// + /// The value of the property after the call to the + /// method made while creating + /// the command. + /// + /// + /// + /// If the method + /// was not invoked, for example because the method has not been + /// called, no command name was specified, an unknown command name was specified, or the + /// command used the interface , the value of the + /// property will be + /// . + /// + /// + public ParseResult ParseResult { get; private set; } + + /// + /// Gets the kind of used to supply the commands. + /// + /// + /// One of the values of the enumeration. + /// + public ProviderKind ProviderKind => _provider.Kind; - /// - /// Gets the options used by this instance. - /// - /// - /// An instance of the class. - /// - /// - /// - /// Modifying the options will change the way this instance behaves. - /// - /// - /// Once a command is created, the instance may be modified - /// with the options of the attribute applied to the - /// command class. Be aware of this if reusing the same or - /// instance to create multiple commands. - /// - /// - public CommandOptions Options => _options; - - /// - /// Gets the result of parsing the arguments for the last call to . - /// - /// - /// The value of the property after the call to the - /// method made while creating - /// the command. - /// - /// - /// - /// If the was not invoked, for - /// example because the method has not been called, no - /// command name was specified, an unknown command name was specified, or the command used - /// custom parsing, the value of the property will be - /// . - /// - /// - public ParseResult ParseResult { get; private set; } - - /// - /// Gets information about the commands. - /// - /// - /// Information about every subcommand defined in the assemblies, ordered by command name. - /// - /// - /// - /// Commands that don't meet the criteria of the - /// predicate are not returned. - /// - /// - /// The automatic version command is added if the - /// property is and there is no command with a conflicting name. - /// - /// - public IEnumerable GetCommands() + /// + /// Gets information about all the commands managed by this instance. + /// + /// + /// Information about every subcommand defined in the assemblies, ordered by command name. + /// + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not returned. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are returned. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are returned. + /// + /// + /// The automatic version command is returned if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is returned. + /// + /// + public IEnumerable GetCommands() + { + var commands = GetCommandsUnsortedAndFiltered(); + if (_options.AutoVersionCommand && + _options.ParentCommand == null && + !commands.Any(c => c.MatchesName(_options.StringProvider.AutomaticVersionCommandName()))) { - var commands = GetCommandsUnsorted(); - if (_options.AutoVersionCommand && - !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) + var versionCommand = CommandInfo.GetAutomaticVersionCommand(this); + if (Options.CommandFilter?.Invoke(versionCommand) ?? true) { - var versionCommand = CommandInfo.GetAutomaticVersionCommand(this); commands = commands.Append(versionCommand); } - - return commands.OrderBy(c => c.Name, _options.CommandNameComparer); } - /// - /// Gets the subcommand with the specified command name. - /// - /// The name of the subcommand. - /// - /// A instance for the specified subcommand, or - /// if none could be found. - /// - /// - /// is . - /// - /// - /// - /// The command is located by searching all types in the assemblies for a command type - /// whose command name matches the specified name. If there are multiple commands with - /// the same name, the first matching one will be returned. - /// - /// - /// A command's name is taken from the property. If - /// that property is , the name is determined by taking the command - /// type's name, and applying the transformation specified by the - /// property. - /// - /// - /// Commands that don't meet the criteria of the - /// predicate are not returned. - /// - /// - /// The automatic version command is returned if the - /// property is and the matches the - /// name of the automatic version command, and not any other command name. - /// - /// - public CommandInfo? GetCommand(string commandName) - { - if (commandName == null) - { - throw new ArgumentNullException(nameof(commandName)); - } - - var commands = GetCommandsUnsorted() - .Where(c => c.MatchesName(commandName, _options.CommandNameComparer)); + return commands.OrderBy(c => c.Name, _options.CommandNameComparison.GetComparer()); + } - if (commands.Any()) - { - return commands.First(); - } + /// + /// Gets the subcommand with the specified command name. + /// + /// The name of the subcommand. + /// + /// A instance for the specified subcommand, or + /// if none could be found. + /// + /// + /// is . + /// + /// + /// + /// The command is located by searching all types in the assemblies for a command type + /// whose command name or alias matches the specified name. If there are multiple commands + /// with the same name, the first matching one will be returned. + /// + /// + /// If the property is , + /// this function will also return a command whose name or alias starts with + /// . In this case, the command will only be returned if there + /// is exactly one matching command; if the prefix is ambiguous, is + /// returned. + /// + /// + /// A command's name is taken from the property. If + /// that property is , the name is determined by taking the command + /// type's name, and applying the transformation specified by the + /// property. A command's aliases are specified using the + /// attribute. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not returned. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are returned. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are returned. + /// + /// + /// The automatic version command is returned if the + /// property is and matches the name of + /// the automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is returned. + /// + /// + public CommandInfo? GetCommand(string commandName) + { + if (commandName == null) + { + throw new ArgumentNullException(nameof(commandName)); + } - if (_options.AutoVersionCommand && - _options.CommandNameComparer.Compare(commandName, _options.AutoVersionCommandName()) == 0) + var commands = GetCommandsUnsortedAndFiltered(); + CommandInfo? versionCommand = null; + if (_options.AutoVersionCommand && _options.ParentCommand == null) + { + // We can append this without checking for duplicates since it will not be checked if an + // earlier command matches. + versionCommand = CommandInfo.GetAutomaticVersionCommand(this); + if (_options.CommandFilter?.Invoke(versionCommand) ?? true) { - return CommandInfo.GetAutomaticVersionCommand(this); + commands = commands.Append(versionCommand); } - - return null; } - /// - /// Finds and instantiates the subcommand with the specified name, or if that fails, writes - /// error and usage information. - /// - /// The name of the command. - /// The arguments to the command. - /// The index in at which to start parsing the arguments. - /// - /// An instance a class implement the interface, or - /// if the command was not found or an error occurred parsing the arguments. - /// - /// - /// is - /// - /// - /// does not fall inside the bounds of . - /// - /// - /// - /// If the command could not be found, a list of possible commands is written using the - /// . If an error occurs parsing the command's arguments, - /// the error message is written to , and the - /// command's usage information is written to . - /// - /// - /// If the parameter is , output is - /// written to a for the standard error stream, - /// wrapping at the console's window width. If the stream is redirected, output may still - /// be wrapped, depending on the value returned by . - /// - /// - /// Commands that don't meet the criteria of the - /// predicate are not returned. - /// - /// - /// The automatic version command is returned if the - /// property is and the command name matches the name of the - /// automatic version command, and not any other command name. - /// - /// - public ICommand? CreateCommand(string? commandName, string[] args, int index) + CommandInfo? partialMatch = null; + var ambiguousMatch = false; + foreach (var command in commands) { - if (args == null) + // Check for an exact match. + if (command.MatchesName(commandName)) { - throw new ArgumentNullException(nameof(args)); + return command; } - if (index < 0 || index > args.Length) + // Check for a prefix match, if requested. + if (Options.AutoCommandPrefixAliases && !ambiguousMatch && command.MatchesPrefix(commandName)) { - throw new ArgumentOutOfRangeException(nameof(index)); + if (partialMatch == null) + { + partialMatch = command; + } + else if (command != versionCommand || !partialMatch.MatchesName(Options.StringProvider.AutomaticVersionCommandName())) + { + // The prefix is ambigious, so don't use it. + // N.B. This doesn't apply if this is the automatic version command and the + // existing match would override the existence of that command. + partialMatch = null; + ambiguousMatch = true; + } } + } - ParseResult = default; - var commandInfo = commandName == null - ? null - : GetCommand(commandName); + return partialMatch; + } - if (commandInfo is not CommandInfo info) - { - WriteUsage(); - return null; - } + /// + /// Finds and instantiates the subcommand with the specified name, or if that fails, writes + /// error and usage information. + /// + /// The name of the command. + /// The arguments to the command. + /// + /// An instance of a class implementing the interface, or + /// if the command was not found or an error occurred parsing the + /// arguments. + /// + /// + /// + /// If the command could not be found, a list of possible commands is written using the + /// property. If an error occurs + /// parsing the command's arguments, the error message is written to the stream indicated by + /// the property, and the command's usage + /// information is written using the + /// property. + /// + /// + /// If the property is , + /// output is written to a instance for the standard + /// error stream (, wrapping at the console's + /// window width. If the stream is redirected, output may still be wrapped, depending on the + /// value returned by . + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not returned. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are returned. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are returned. + /// + /// + /// The automatic version command is returned if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is returned. + /// + /// + public ICommand? CreateCommand(string? commandName, ReadOnlyMemory args) + { + ParseResult = default; + var commandInfo = commandName == null + ? null + : GetCommand(commandName); - _options.UsageWriter.CommandName = info.Name; - try - { - var (command, result) = info.CreateInstanceWithResult(args, index); - ParseResult = result; - return command; - } - finally - { - _options.UsageWriter.CommandName = null; - } + if (commandInfo is not CommandInfo info) + { + WriteUsage(); + return null; } - /// - /// - /// Finds and instantiates the subcommand with the name from the first argument, or if that - /// fails, writes error and usage information. - /// - public ICommand? CreateCommand(string[] args, int index = 0) + _options.UsageWriter.CommandName = info.Name; + try { - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - string? commandName = null; - if (index < args.Length) - { - commandName = args[index]; - ++index; - } - - return CreateCommand(commandName, args, index); + var (command, result) = info.CreateInstanceWithResult(args); + ParseResult = result; + return command; } + finally + { + _options.UsageWriter.CommandName = null; + } + } - /// - /// Finds and instantiates the subcommand using the arguments from , - /// using the first argument for the command name. If that fails, writes error and usage information. - /// - /// - /// - /// - /// - /// - /// - public ICommand? CreateCommand() + /// + /// The name of the command. + /// The arguments to the command. + /// + /// is . + /// + public ICommand? CreateCommand(string? commandName, string[] args) + { + if (args == null) { - // Skip the first argument, it's the application name. - return CreateCommand(Environment.GetCommandLineArgs(), 1); + throw new ArgumentNullException(nameof(args)); } + return CreateCommand(commandName, args.AsMemory()); + } - /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it. If it fails, writes error and usage information. - /// - /// The name of the command. - /// The arguments to the command. - /// The index in at which to start parsing the arguments. - /// - /// The value returned by , or if - /// the command could not be created. - /// - /// - /// is - /// - /// - /// does not fall inside the bounds of . - /// - /// - /// - /// This function creates the command by invoking the , - /// method and then invokes the method on the command. - /// - /// - public int? RunCommand(string? commandName, string[] args, int index) + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, or if that + /// fails, writes error and usage information. + /// + /// + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. + /// + /// + /// is . + /// + public ICommand? CreateCommand(string[] args) + { + if (args == null) { - var command = CreateCommand(commandName, args, index); - return command?.Run(); + throw new ArgumentNullException(nameof(args)); } - /// - /// - /// Finds and instantiates the subcommand with the name from the first argument, and if it - /// succeeds, runs it. If it fails, writes error and usage information. - /// - /// - /// - /// This function creates the command by invoking the , - /// method and then invokes the method on the command. - /// - /// - public int? RunCommand(string[] args, int index = 0) + return CreateCommand(args.AsMemory()); + } + + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, or if that + /// fails, writes error and usage information. + /// + /// + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. + /// + public ICommand? CreateCommand(ReadOnlyMemory args) + { + string? commandName = null; + if (args.Length != 0) { - var command = CreateCommand(args, index); - return command?.Run(); + commandName = args.Span[0]; + args = args.Slice(1); } - /// - /// Finds and instantiates the subcommand using the arguments from the - /// method, using the first argument as the command name. If it succeeds, runs the command. - /// If it fails, writes error and usage information. - /// - /// - /// - /// - /// - /// - /// This function creates the command by invoking the , - /// method and then invokes the method on the command. - /// - /// - public int? RunCommand() + return CreateCommand(commandName, args); + } + + /// + /// Finds and instantiates the subcommand using the arguments from the + /// method, using the first argument for the command name. If that fails, writes error and usage + /// information. + /// + /// + /// + /// + /// + /// + /// + public ICommand? CreateCommand() + { + // Skip the first argument, it's the application name. + return CreateCommand(Environment.GetCommandLineArgs().AsMemory(1)); + } + + + /// + /// Finds and instantiates the subcommand with the specified name, and if it succeeds, + /// runs it. If it fails, writes error and usage information. + /// + /// The name of the command. + /// The arguments to the command. + /// + /// The value returned by , or if + /// the command could not be created. + /// + /// + /// is + /// + /// + /// + /// This function creates the command by invoking the + /// method, and then invokes the method on the command. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the is , only + /// commands without a attribute are included. If it is + /// not , only commands where the type specified using the + /// attribute matches the value of the property are + /// included. + /// + /// + public int? RunCommand(string? commandName, string[] args) + { + var command = CreateCommand(commandName, args); + return command?.Run(); + } + + /// + /// Finds and instantiates the subcommand with the specified name, and if it succeeds, + /// runs it. If it fails, writes error and usage information. + /// + /// The name of the command. + /// The arguments to the command. + /// + /// The value returned by , or if + /// the command could not be created. + /// + /// + /// + /// This function creates the command by invoking the + /// method, and then invokes the method on the command. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the is , only + /// commands without a attribute are included. If it is + /// not , only commands where the type specified using the + /// attribute matches the value of the property are + /// included. + /// + /// + public int? RunCommand(string? commandName, ReadOnlyMemory args) + { + var command = CreateCommand(commandName, args); + return command?.Run(); + } + + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, and if it + /// succeeds, runs it. If it fails, writes error and usage information. + /// + /// + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. + /// + /// + /// + /// This function creates the command by invoking the + /// method, and then invokes the method on the command. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the is , only + /// commands without a attribute are included. If it is + /// not , only commands where the type specified using the + /// attribute matches the value of the property are + /// included. + /// + /// + public int? RunCommand(ReadOnlyMemory args) + { + var command = CreateCommand(args); + return command?.Run(); + } + + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, and if it + /// succeeds, runs it. If it fails, writes error and usage information. + /// + /// + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. + /// + /// + /// + /// This function creates the command by invoking the + /// method, and then invokes the method on the command. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the is , only + /// commands without a attribute are included. If it is + /// not , only commands where the type specified using the + /// attribute matches the value of the property are + /// included. + /// + /// + public int? RunCommand(string[] args) + { + var command = CreateCommand(args); + return command?.Run(); + } + + /// + /// Finds and instantiates the subcommand using the arguments from the + /// method, using the first argument as the command name. If it succeeds, runs the command. + /// If it fails, writes error and usage information. + /// + /// + /// + /// + /// + /// + /// This function creates the command by invoking the method, + /// and then invokes the method on the command. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public int? RunCommand() + { + // Skip the first argument, it's the application name. + return RunCommand(Environment.GetCommandLineArgs().AsMemory(1)); + } + + /// + /// + /// Finds and instantiates the subcommand with the specified name, and if it succeeds, + /// runs it asynchronously. If it fails, writes error and usage information. + /// + /// + /// A task representing the asynchronous run operation. The result is the value returned + /// by , or if the command + /// could not be created. + /// + /// + /// + /// This function creates the command by invoking the + /// method. 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 + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public async Task RunCommandAsync(string? commandName, ReadOnlyMemory args) + { + var command = CreateCommand(commandName, args); + if (command is IAsyncCommand asyncCommand) { - // Skip the first argument, it's the application name. - return RunCommand(Environment.GetCommandLineArgs(), 1); + return await asyncCommand.RunAsync(); } - /// - /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it asynchronously. If it fails, writes error and usage information. - /// - /// - /// A task representing the asynchronous run operation. The result is the value returned - /// by , or if the command - /// could not be created. - /// - /// - /// - /// This function creates the command by invoking the , - /// method. If the command implements the interface, it - /// invokes the method; otherwise, it invokes the - /// method on the command. - /// - /// - public async Task RunCommandAsync(string? commandName, string[] args, int index) - { - var command = CreateCommand(commandName, args, index); - if (command is IAsyncCommand asyncCommand) - { - return await asyncCommand.RunAsync(); - } + return command?.Run(); + } - return command?.Run(); + /// + /// + /// Finds and instantiates the subcommand with the specified name, and if it succeeds, + /// runs it asynchronously. If it fails, writes error and usage information. + /// + /// + /// A task representing the asynchronous run operation. The result is the value returned + /// by , or if the command + /// could not be created. + /// + /// + /// + /// This function creates the command by invoking the + /// method. 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 + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public async Task RunCommandAsync(string? commandName, string[] args) + { + var command = CreateCommand(commandName, args); + if (command is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); } - /// - /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it asynchronously. If it fails, writes error and usage information. - /// - /// - /// - /// This function creates the command by invoking the , - /// method. If the command implements the interface, it - /// invokes the method; otherwise, it invokes the - /// method on the command. - /// - /// - public async Task RunCommandAsync(string[] args, int index = 0) - { - var command = CreateCommand(args, index); - if (command is IAsyncCommand asyncCommand) - { - return await asyncCommand.RunAsync(); - } + return command?.Run(); + } - return command?.Run(); + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, and if it + /// succeeds, runs it asynchronously. If it fails, writes error and usage information. + /// + /// + /// + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. + /// + /// + /// This function creates the command by invoking the + /// method. 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 + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public async Task RunCommandAsync(ReadOnlyMemory args) + { + var command = CreateCommand(args); + if (command is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); } - /// - /// - /// Finds and instantiates the subcommand using the arguments from the - /// method, using the first argument as the command name. If it succeeds, runs the command - /// asynchronously. If it fails, writes error and usage information. - /// - /// - /// - /// This function creates the command by invoking the , - /// method. If the command implements the interface, it - /// invokes the method; otherwise, it invokes the - /// method on the command. - /// - /// - public async Task RunCommandAsync() - { - var command = CreateCommand(); - if (command is IAsyncCommand asyncCommand) - { - return await asyncCommand.RunAsync(); - } + return command?.Run(); + } - return command?.Run(); + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, and if it + /// succeeds, runs it asynchronously. If it fails, writes error and usage information. + /// + /// + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. + /// + /// + /// + /// This function creates the command by invoking the + /// method. 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 + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public async Task RunCommandAsync(string[] args) + { + var command = CreateCommand(args); + if (command is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); } - /// - /// Writes usage help with a list of all the commands. - /// - /// - /// - /// This method writes usage help for the application, including a list of all shell - /// command names and their descriptions to . - /// - /// - /// A command's name is retrieved from its attribute, - /// and the description is retrieved from its attribute. - /// - /// - public void WriteUsage() + return command?.Run(); + } + + /// + /// Finds and instantiates the subcommand using the arguments from the + /// method, using the first argument as the command name. If it succeeds, runs the command + /// asynchronously. If it fails, writes error and usage information. + /// + /// + /// + /// + /// + /// + /// This function creates the command by invoking the + /// method. 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 + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public async Task RunCommandAsync() + { + var command = CreateCommand(); + if (command is IAsyncCommand asyncCommand) { - _options.UsageWriter.WriteCommandListUsage(this); + return await asyncCommand.RunAsync(); } - /// - /// Gets a string with the usage help with a list of all the commands. - /// - /// A string containing the usage help. - /// - /// - /// A command's name is retrieved from its attribute, - /// and the description is retrieved from its attribute. - /// - /// - public string GetUsage() + return command?.Run(); + } + + /// + /// Writes usage help with a list of all the commands. + /// + /// + /// + /// This method writes usage help for the application, including a list of all + /// subcommand names and their descriptions using the + /// property. + /// + /// + /// A command's name is retrieved from its attribute, + /// and the description is retrieved from its attribute. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public void WriteUsage() + { + _options.UsageWriter.WriteCommandListUsage(this); + } + + /// + /// Gets a string with the usage help with a list of all the commands. + /// + /// A string containing the usage help. + /// + /// + /// A command's name is retrieved from its attribute, + /// and the description is retrieved from its attribute. + /// + /// + /// Commands that don't meet the criteria of the + /// predicate are not included. + /// + /// + /// If the property is + /// , only commands without a + /// attribute are included. If it is not , only commands where the type + /// specified using the attribute matches the value of + /// the property are included. + /// + /// + /// The automatic version command is included if the + /// property is and the command name matches the name of the + /// automatic version command, and not any other command name. The + /// and property also affect + /// whether the version command is included. + /// + /// + public string GetUsage() + { + return _options.UsageWriter.GetCommandListUsage(this); + } + + /// + /// Gets the application description that will optionally be included in the usage help. + /// + /// + /// If the property is not , + /// and the command type referenced has the attribute, the + /// description given in that attribute. Otherwise, the value of the + /// for the first assembly used by this instance. + /// + /// + public string? GetApplicationDescription() + { + var attribute = _options.ParentCommand?.GetCustomAttribute(); + if (attribute != null) { - return _options.UsageWriter.GetCommandListUsage(this); + return attribute.Description; } - /// - /// Gets the application description that will optionally be included in the usage help. - /// - /// - /// The value of the for the first assembly - /// used by this instance. - /// - public string? GetApplicationDescription() - => (_assembly ?? _assemblies?.FirstOrDefault())?.GetCustomAttribute()?.Description; - - // Return value does not include the automatic version command. - private IEnumerable GetCommandsUnsorted() - { - IEnumerable types; - if (_assembly != null) - { - types = _assembly.GetTypes(); - } - else - { - Debug.Assert(_assemblies != null); - types = _assemblies.SelectMany(a => a.GetTypes()); - } + return _provider.GetApplicationDescription(); + } - return from type in types - let info = CommandInfo.TryCreate(type, this) - where info != null && (_options.CommandFilter?.Invoke(info.Value) ?? true) - select info.Value; + private IEnumerable GetCommandsUnsortedAndFiltered() + { + var commands = _provider.GetCommandsUnsorted(this).Where(c => c.ParentCommandType == _options.ParentCommand); + if (_options.CommandFilter != null) + { + commands = commands.Where(c => _options.CommandFilter(c)); } + + return commands; } } diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 8d121f57..41b1753b 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -1,117 +1,228 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; -using System.Collections.Generic; +using System; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Provides options for the class. +/// +/// +public class CommandOptions : ParseOptions { /// - /// Provides options for the class. + /// Gets or sets a value that indicates whether the options follow POSIX conventions. /// - public class CommandOptions : ParseOptions + /// + /// if the options follow POSIX conventions; otherwise, + /// . + /// + /// + /// + /// This property is provided as a convenient way to set a number of related properties + /// that together indicate the parser is using POSIX conventions. POSIX conventions in + /// this case means that parsing uses long/short mode, argument and command names are case + /// sensitive, and argument names, command names and value descriptions use dash-case + /// (e.g. "command-name"). + /// + /// + /// Setting this property to is equivalent to setting the + /// property to , the + /// property to , + /// the property to , + /// the property to , + /// the property to , + /// and the property to . + /// + /// + /// This property will only return if the above properties are the + /// indicated values, except that the and + /// properties can be any case-sensitive comparison. It + /// will return for any other combination of values, not just the ones + /// indicated below. + /// + /// + /// Setting this property to is equivalent to setting the + /// property to , the + /// property to , + /// the property to , + /// the property to , + /// the property to , + /// and the property to . + /// + /// + public override bool IsPosix { - /// - /// Gets or sets the used to compare command names. - /// - /// - /// The used to compare command names. The default value is . - /// - public IComparer CommandNameComparer { get; set; } = StringComparer.OrdinalIgnoreCase; + get => base.IsPosix && CommandNameComparison.IsCaseSensitive() && CommandNameTransform == NameTransform.DashCase; + set + { + base.IsPosix = value; + if (value) + { + CommandNameComparison = StringComparison.InvariantCulture; + CommandNameTransform = NameTransform.DashCase; + } + else + { + CommandNameComparison = StringComparison.OrdinalIgnoreCase; + CommandNameTransform = NameTransform.None; + } + } + } + + /// + /// Gets or sets the type of string comparison to use for argument names. + /// + /// + /// One of the values of the enumeration. The default value + /// is . + /// + public StringComparison CommandNameComparison { get; set; } = StringComparison.OrdinalIgnoreCase; + + /// + /// Gets or sets a value that indicates how names are created for commands that don't have + /// an explicit name. + /// + /// + /// One of the values of the enumeration. The default value + /// is . + /// + /// + /// + /// If a command hasn't set an explicit name using the + /// attribute, the name is derived from the type name of the command, applying the + /// specified transformation. + /// + /// + /// If this property is not , the value specified by the + /// property will be removed from the end of the + /// type name before applying the transformation. + /// + /// + /// This transformation is also used for the name of the automatic version command if + /// the property is . + /// + /// + /// This transformation is not used for commands that have an explicit name. + /// + /// + public NameTransform CommandNameTransform { get; set; } + + /// + /// Gets or sets a value that will be removed from the end of a command name during name + /// transformation. + /// + /// + /// The suffix to remove, or to not remove any suffix. The default + /// value is "Command". + /// + /// + /// + /// This property is only used if the property is not + /// , and is never used for commands with an explicit + /// name. + /// + /// + /// For example, if you have a subcommand class named CreateFileCommand and you use + /// and the default value of "Command" + /// for this property, the name of the command will be "create-file" without having to + /// explicitly specify it. + /// + /// + /// The value of this property is case sensitive. + /// + /// + public string? StripCommandNameSuffix { get; set; } = "Command"; - /// - /// Gets or sets a value that indicates how names are created for commands that don't have - /// an explicit name. - /// - /// - /// One of the values of the enumeration. The default value - /// is . - /// - /// - /// - /// If a command hasn't set an explicit name using the - /// attribute, the name is derived from the type name of the command, applying the - /// specified transformation. - /// - /// - /// If this property is not , the value specified by the - /// property will be removed from the end of the - /// type name before applying the transformation. - /// - /// - /// This transformation is also used for the name of the automatic version command if - /// the property is . - /// - /// - /// This transformation is not used for commands that have an explicit name. - /// - /// - public NameTransform CommandNameTransform { get; set; } + /// + /// Gets or sets a function that filters which commands to include. + /// + /// + /// A function that filters the commands, or to use no filter. The + /// default value is . + /// + /// + /// + /// Return from the filter predicate to include a command, and + /// to exclude it. If this property is , all + /// commands will be included. + /// + /// + /// Use this filter to only use a subset of the commands defined in the assembly or + /// assemblies. The commands that do not match this filter cannot be invoked by the end user, + /// and will not be returned by the methods of the class. + /// + /// + public Func? CommandFilter { get; set; } - /// - /// Gets or sets a value that will be removed from the end of a command name during name - /// transformation. - /// - /// - /// The suffix to remove, or to not remove any suffix. The default - /// value is "Command". - /// - /// - /// - /// This property is only used if the property is not - /// , and is never used for commands with an explicit - /// name. - /// - /// - /// For example, if you have a subcommand class named "CreateFileCommand" and you use - /// and the default value of "Command" for this - /// property, the name of the command will be "create-file" without having to explicitly - /// specify it. - /// - /// - /// The suffix is case sensitive. - /// - /// - public string? StripCommandNameSuffix { get; set; } = "Command"; + /// + /// Gets or sets the parent command to filter commands by. + /// + /// + /// The of a command whose children should be used by the + /// class, or to use commands without a parent. + /// + /// + /// + /// The class will only consider commands whose parent, as + /// set using the attribute, matches this type. If + /// this property is , only commands that do not have a the + /// attribute are considered. + /// + /// + /// All other commands are filtered out and will not be returned, created, or executed + /// by the class. + /// + /// + public Type? ParentCommand { get; set; } - /// - /// Gets or sets a function that filters which commands to include. - /// - /// - /// A function that filters the commands, or to use no filter. The - /// default value is . - /// - /// - /// - /// Use this to only use a subset of the commands defined in the assembly or assemblies. - /// The remaining commands will not be possible to invoke by the user. - /// - /// - /// The filter is not invoked for the automatic version command. Set the - /// property to if you wish to exclude that command. - /// - /// - public Func? CommandFilter { get; set; } + /// + /// Gets or sets a value that indicates whether a version command should automatically be + /// created. + /// + /// + /// to automatically create a version command; otherwise, + /// . The default is . + /// + /// + /// + /// If this property is true, a command named "version" will be automatically added to + /// the list of available commands, unless a command with that name already exists. + /// + /// + /// When invoked, the command will show version information for the application, based + /// on the entry point assembly. + /// + /// + /// You can customize the name and description of the automatic version command using the + /// class. + /// + /// + /// + /// + public bool AutoVersionCommand { get; set; } = true; - /// - /// Gets or sets a value that indicates whether a version command should automatically be - /// created. - /// - /// - /// to automatically create a version command; otherwise, - /// . The default is . - /// - /// - /// - /// If this property is true, a command named "version" will be automatically added to - /// the list of available commands, unless a command with that name already exists. - /// When invoked, the command will show version information for the application, based - /// on the entry point assembly. - /// - /// - public bool AutoVersionCommand { get; set; } = true; + /// + /// Gets or sets a value that indicates whether unique prefixes of a command name or alias are + /// automatically used as aliases. + /// + /// + /// to automatically use unique prefixes of a command as aliases + /// for that argument; otherwise . The default value is + /// . + /// + /// + /// + /// If this property is , the class + /// will consider any prefix that uniquely identifies a command by its name or one of its + /// explicit aliases as an alias for that command. For example, given two commands "read" + /// and "record", "rea" would be an alias for "read", and "rec" an alias for + /// "record" (as well as "reco" and "recor"). Both "r" and "re" would not be an alias + /// because they don't uniquely identify a single command. + /// + /// + public bool AutoCommandPrefixAliases { get; set; } = true; - internal string AutoVersionCommandName() - { - return CommandNameTransform.Apply(StringProvider.AutomaticVersionCommandName()); - } + internal string AutoVersionCommandName() + { + return CommandNameTransform.Apply(StringProvider.AutomaticVersionCommandName()); } } diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs new file mode 100644 index 00000000..fe8ea97d --- /dev/null +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace Ookii.CommandLine.Commands; + +/// +/// Indicates that the target class is a command manager created using source generation. +/// +/// +/// +/// When using this attribute, source generation is used to determine which classes are available +/// at compile time, either in the assembly being compiled, or the assemblies specified using the +/// property. The target class will be modified to inherit from the +/// class, and should be used instead of the +/// class to find, create, and run commands. +/// +/// +/// Using a class with this attribute avoids the use of runtime reflection to determine which +/// commands are available, improving performance and allowing your application to be trimmed. +/// +/// +/// To use source generation for the command line arguments of individual commands, use the +/// attribute on each command class. +/// +/// +/// +/// Source generation +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class GeneratedCommandManagerAttribute : Attribute +{ + /// + /// Gets or sets the names of the assemblies that contain the commands that the generated + /// will use. + /// + /// + /// An array with assembly names, or to use the commands from the + /// assembly containing the generated manager. + /// + /// + /// + /// The assemblies used must be directly referenced by your project. Dynamically loading + /// assemblies is not supported by this attribute; use the + /// + /// constructor instead for that purpose. + /// + /// + /// The names in this array can be either just the assembly name, or the full assembly + /// identity including version, culture, and public key token. + /// + /// + public string[]? AssemblyNames { get; set; } +} diff --git a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index efd93590..51150529 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -1,38 +1,42 @@ using System.Threading.Tasks; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Represents a subcommand that executes asynchronously. +/// +/// +/// +/// 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. +/// +/// +/// Use the class as a base class for your command to get a default +/// implementation of the +/// +/// +public interface IAsyncCommand : ICommand { /// - /// Represents a subcommand that executes asynchronously. + /// Runs the command asynchronously. /// + /// + /// A task that represents the asynchronous run operation. The result of the task is the + /// exit code for the command. + /// /// /// - /// 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. + /// 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. /// /// - public interface IAsyncCommand : ICommand - { - /// - /// Runs the command asynchronously. - /// - /// - /// A task that represents the asynchronous run operation. The result of the task is the - /// exit code for the command. - /// - /// - /// - /// Typically, your applications 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 task. Use the - /// class for a default implementation that does this. - /// - /// - Task RunAsync(); - } + Task RunAsync(); } diff --git a/src/Ookii.CommandLine/Commands/ICommand.cs b/src/Ookii.CommandLine/Commands/ICommand.cs index d498619f..e5562434 100644 --- a/src/Ookii.CommandLine/Commands/ICommand.cs +++ b/src/Ookii.CommandLine/Commands/ICommand.cs @@ -1,35 +1,31 @@ -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Represents a subcommand of the application. +/// +/// +/// +/// To create a subcommand for your application, create a class that implements this interface, +/// then apply the attribute to it. +/// +/// +/// The class will be used as an arguments type with the . +/// Alternatively, a command can implement its own argument parsing by implementing the +/// interface. +/// +/// +/// +public interface ICommand { /// - /// Represents a subcommand of the application. + /// Runs the command. /// + /// The exit code for the command. /// /// - /// To create a subcommand for your application, create a class that implements this interface, - /// then apply the attribute to it. - /// - /// - /// The class will be used as an arguments type with the , so - /// it can define command line arguments using its properties and constructor parameters. - /// - /// - /// Alternatively, a command can implement its own argument parsing by implementing the - /// interface. + /// Typically, your application's Main() method should return the exit code of the + /// command that was executed. /// /// - /// - public interface ICommand - { - /// - /// Runs the command. - /// - /// The exit code for the command. - /// - /// - /// Typically, your applications Main() method should return the exit code of the - /// command that was executed. - /// - /// - int Run(); - } + int Run(); } diff --git a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs index 9df820a0..188af561 100644 --- a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs @@ -1,26 +1,26 @@ -namespace Ookii.CommandLine.Commands +using System; + +namespace Ookii.CommandLine.Commands; + +/// +/// Represents a subcommand that does its own argument parsing. +/// +/// +/// +/// Unlike commands that only implement the interface, commands that +/// implement the interface are not created with the +/// class. Instead, they must have a public constructor with no +/// parameters, and must parse the arguments manually by implementing the +/// method. +/// +/// +/// +public interface ICommandWithCustomParsing : ICommand { /// - /// Represents a subcommand that does its own argument parsing. + /// Parses the arguments for the command. /// - /// - /// - /// Unlike commands that only implement the interfaces, commands that - /// implement the interface are not created with the - /// . Instead, they must have a public constructor with no - /// parameters, and must parse the arguments manually by implementing the - /// method. - /// - /// - /// - public interface ICommandWithCustomParsing : ICommand - { - /// - /// Parses the arguments for the command. - /// - /// The arguments. - /// The index of the first argument. - /// The options to use for parsing and usage help. - void Parse(string[] args, int index, CommandOptions options); - } + /// The arguments for the command. + /// The that was used to create this command. + void Parse(ReadOnlyMemory args, CommandManager manager); } diff --git a/src/Ookii.CommandLine/Commands/ParentCommand.cs b/src/Ookii.CommandLine/Commands/ParentCommand.cs new file mode 100644 index 00000000..890e3f60 --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -0,0 +1,256 @@ +using System; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Commands; + +/// +/// Base class for subcommands that have nested subcommands. +/// +/// +/// +/// The class, along with the +/// attribute, aid in easily creating applications that contain nested subcommands. This class +/// handles finding, creating and running any nested subcommands, and handling parsing errors and +/// printing usage help for those subcommands. +/// +/// +/// To utilize this class, derive a class from this class and apply the +/// attribute to that class. Then, apply the attribute to any +/// child commands of this command. +/// +/// +/// Often, the derived class can be empty; however, you can override the members of this class +/// to customize the behavior. +/// +/// +/// The class is based on the +/// interface, so derived classes cannot define any arguments or use other functionality that +/// depends on the class. +/// +/// +/// +public abstract class ParentCommand : ICommandWithCustomParsing, IAsyncCommand +{ + private ICommand? _childCommand; + + /// + /// Gets the exit code to return from the or method + /// if parsing command line arguments for a nested subcommand failed. + /// + /// + /// The exit code to use for parsing failure. The base class implementation returns 1. + /// + protected virtual int FailureExitCode => 1; + + /// + /// Parses the arguments for the command, locating and instantiating a child command. + /// + /// + /// The arguments for the command, where the first argument is the name of the child command. + /// + /// + /// The instance that was used to create this command. + /// + public void Parse(ReadOnlyMemory args, CommandManager manager) + { + OnModifyOptions(manager.Options); + var originalParentCommand = manager.Options.ParentCommand; + manager.Options.ParentCommand = GetType(); + CommandInfo? info; + try + { + var childCommandName = args.Length == 0 ? null : args.Span[0]; + info = childCommandName == null ? null : manager.GetCommand(childCommandName); + if (info == null) + { + OnChildCommandNotFound(childCommandName, manager); + return; + } + } + finally + { + manager.Options.ParentCommand = originalParentCommand; + } + + args = args.Slice(1); + var originalCommandName = manager.Options.UsageWriter.CommandName; + manager.Options.UsageWriter.CommandName = originalCommandName == null ? info.Name : originalCommandName + ' ' + info.Name; + try + { + if (info.UseCustomArgumentParsing) + { + var command = info.CreateInstanceWithCustomParsing(); + command.Parse(args, manager); + _childCommand = command; + OnAfterParsing(null, command); + return; + } + + var parser = info.CreateParser(); + EventHandler? handler = null; + if (parser.Options.DuplicateArgumentsOrDefault == ErrorMode.Warning) + { + handler = (sender, e) => + { + e.KeepOldValue = !OnDuplicateArgumentWarning(e.Argument, e.NewValue); + }; + + parser.DuplicateArgument += handler; + } + + try + { + _childCommand = (ICommand?)parser.Parse(args); + } + catch (CommandLineArgumentException) + { + // Handled by OnAfterParsing. + } + + OnAfterParsing(parser, _childCommand); + } + finally + { + manager.Options.UsageWriter.CommandName = originalCommandName; + } + } + + /// + /// Runs the child command that was instantiated by the method. + /// + /// + /// The exit code of the child command, or the value of the + /// property if no child command was created. + /// + public virtual int Run() + { + if (_childCommand == null) + { + return FailureExitCode; + } + + return _childCommand.Run(); + } + + /// + /// Runs the child command that was instantiated by the method asynchronously. + /// + /// + /// A task that represents the asynchronous run operation. The result of the task is the exit + /// code of the child command, or the value of the property if no + /// child command was created. + /// + public virtual async Task RunAsync() + { + if (_childCommand == null) + { + return FailureExitCode; + } + + if (_childCommand is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); + } + + return _childCommand.Run(); + } + + /// + /// Allows derived classes to customize the command and parse options used for the nested + /// subcommands. + /// + /// The . + /// + /// + /// The base class implementation does nothing. + /// + /// + protected virtual void OnModifyOptions(CommandOptions options) + { + // Intentionally blank + } + + /// + /// Method called when no nested subcommand name was specified, or the nested subcommand + /// could not be found. + /// + /// + /// The name of the nested subcommand, or if none was specified. + /// + /// The used to create the subcommand. + /// + /// + /// The base class implementation writes usage help with a list of all nested subcommands. + /// + /// + protected virtual void OnChildCommandNotFound(string? commandName, CommandManager manager) + { + manager.WriteUsage(); + } + + /// + /// Method called when the property is set to + /// and a duplicate argument value was encountered. + /// + /// The duplicate argument. + /// The new value for the argument. + /// + /// to use the new value for the argument; to + /// keep the old value. The base class implementation always returns . + /// + /// + /// + /// The base class implementation writes a warning to the + /// writer. + /// + /// + /// This method will not be called if the nested subcommand uses the + /// interface. + /// + /// + protected virtual bool OnDuplicateArgumentWarning(CommandLineArgument argument, string? newValue) + { + var parser = argument.Parser; + var warning = parser.StringProvider.DuplicateArgumentWarning(argument.ArgumentName); + CommandLineParser.WriteError(parser.Options, warning, parser.Options.WarningColor); + return true; + } + + /// + /// Function called after parsing, on success, cancellation, and failure. + /// + /// + /// The instance for the nested subcommand, or + /// if the nested subcommand used the interface. + /// + /// + /// The created subcommand class, or if a failure or cancellation was + /// encountered. + /// + /// + /// + /// The base class implementation writes any error message, and usage help for the nested + /// subcommand if applicable. On success, or for nested subcommands using the + /// interface, it does nothing. + /// + /// + protected virtual void OnAfterParsing(CommandLineParser? parser, ICommand? childCommand) + { + if (parser == null) + { + return; + } + + var helpMode = UsageHelpRequest.Full; + if (parser.ParseResult.LastException != null) + { + CommandLineParser.WriteError(parser.Options, parser.ParseResult.LastException.Message, parser.Options.ErrorColor, true); + helpMode = parser.Options.ShowUsageOnError; + } + + if (parser.HelpRequested) + { + parser.Options.UsageWriter.WriteParserUsage(parser, helpMode); + } + } +} diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs new file mode 100644 index 00000000..bdbc6c7c --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Commands; + +/// +/// Indicates the parent command for a nested subcommand. +/// +/// +/// +/// If you wish to have a command with nested subcommands, apply this attribute to the children +/// of another command. The class will only return commands whose +/// property value matches the +/// property. +/// +/// +/// The parent command type should be the type of another command. It may be a command derived +/// from the class, but this is not required. The +/// class makes implementing nested subcommands easy, but you may +/// also use any command with your own nested subcommand logic as a parent command. +/// +/// +/// To create a hierarchy of subcommands, the command with this attribute may itself also have +/// nested subcommands. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class ParentCommandAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The type name of the parent command class. + /// + /// is . + /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// + public ParentCommandAttribute(string parentCommandTypeName) + { + ParentCommandTypeName = parentCommandTypeName ?? throw new ArgumentNullException(nameof(parentCommandTypeName)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The of the parent command class. + /// + /// is . + /// + public ParentCommandAttribute(Type parentCommandType) + { + ParentCommandTypeName = parentCommandType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(parentCommandType)); + } + + /// + /// Gets or sets the name of the parent command type. + /// + /// + /// The type name. + /// + public string ParentCommandTypeName { get; } + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + internal Type GetParentCommandType() => Type.GetType(ParentCommandTypeName, true)!; +} diff --git a/src/Ookii.CommandLine/ConstructorTypeConverter.cs b/src/Ookii.CommandLine/ConstructorTypeConverter.cs deleted file mode 100644 index 7d1d042b..00000000 --- a/src/Ookii.CommandLine/ConstructorTypeConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; - -namespace Ookii.CommandLine -{ - /// - /// Type converter used to instantiate argument types with a string constructor. - /// - internal class ConstructorTypeConverter : TypeConverterBase - { - private readonly Type _type; - - public ConstructorTypeConverter(Type type) - { - _type = type; - } - - protected override object? Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value) - { - try - { - return _type.CreateInstance(value); - } - catch (Exception ex) - { - // Since we don't know what the constructor will throw, we'll wrap anything in a - // FormatException. - throw new FormatException(ex.Message, ex); - } - } - } -} diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs new file mode 100644 index 00000000..a402349d --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Base class for converters from a string to the type of an argument. +/// +/// +/// +/// To create a custom argument converter, you must implement at least the +/// method. If it's possible to +/// convert to the target type from a structure, it's strongly +/// recommended to also implement the +/// method. +/// +/// +/// +public abstract class ArgumentConverter +{ + /// + /// Converts a string to the type of the argument. + /// + /// The string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// or or is + /// . + /// + /// + /// The value was not in a correct format for the target type. + /// + /// + /// The value was out of range for the target type. + /// + /// + /// The value was not in a correct format for the target type. Unlike + /// and , a thrown + /// by this method will be passed down to the user unmodified. + /// + public abstract object? Convert(string value, CultureInfo culture, CommandLineArgument argument); + + /// + /// Converts a string span to the type of the argument. + /// + /// The containing the string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// + /// The default implementation of this method will allocate a string and call + /// . Override this method if + /// a direct conversion from a is possible for the target + /// type. + /// + /// + /// + /// or is . + /// + /// + /// The value was not in a correct format for the target type. + /// + /// + /// The value was out of range for the target type. + /// + /// + /// The value was not in a correct format for the target type. Unlike + /// and , a thrown + /// by this method will be passed down to the user unmodified. + /// + public virtual object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) + { + return Convert(value.ToString(), culture, argument); + } +} diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs new file mode 100644 index 00000000..43c4388a --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Specifies a custom to use for converting the value of an +/// argument from a string. +/// +/// +/// +/// The type specified by this attribute must derive from the +/// class, and must convert to the type of the argument the attribute is applied to. +/// +/// +/// Apply this attribute to the property or method defining an argument to use a custom +/// conversion from a string to the type of the argument. +/// +/// +/// If this attribute is not present, the default conversion will be used. +/// +/// +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class ArgumentConverterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class with the + /// specified converter type. + /// + /// + /// The to use as a converter. + /// + /// + /// is + /// +#if NET6_0_OR_GREATER + public ArgumentConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type converterType) +#else + public ArgumentConverterAttribute(Type converterType) +#endif + { + ConverterTypeName = converterType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(converterType)); + } + + /// + /// Initializes a new instance of the class with the + /// specified converter type name. + /// + /// + /// The fully qualified name of the to use as a converter. + /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// + /// + /// is + /// +#if NET6_0_OR_GREATER + public ArgumentConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] string converterTypeName) +#else + public ArgumentConverterAttribute(string converterTypeName) +#endif + { + ConverterTypeName = converterTypeName ?? throw new ArgumentNullException(nameof(converterTypeName)); + } + + /// + /// Gets the fully qualified name of the to use as a converter. + /// + /// + /// The fully qualified name of the to use as a converter. + /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + public string ConverterTypeName { get; } + + internal Type GetConverterType() + { + return Type.GetType(ConverterTypeName, true)!; + } +} diff --git a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs new file mode 100644 index 00000000..38cfbde2 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Converter for arguments with values. These are typically switch arguments. +/// +/// +/// +/// For a switch argument, the converter is only used if the value was explicitly specified. +/// +/// +/// +public class BooleanConverter : ArgumentConverter +{ + /// + /// A default instance of the converter. + /// + public static readonly BooleanConverter Instance = new(); + + /// + /// Converts a string to a . + /// + /// The string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// + /// This method performs the conversion using the method. + /// + /// + /// + /// is . + /// + /// + /// The value was not in a correct format for the target type. + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => bool.Parse(value); + +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + /// + /// Converts a string span to a . + /// + /// The containing the string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// + /// This method performs the conversion using the method. + /// + /// + /// + /// The value was not in a correct format for the target type. + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) => bool.Parse(value); +#endif +} diff --git a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs new file mode 100644 index 00000000..e2fe68fb --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs @@ -0,0 +1,47 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.ExceptionServices; + +namespace Ookii.CommandLine.Conversion; + +internal class ConstructorConverter : ArgumentConverter +{ +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + + private readonly Type _type; + + public ConstructorConverter( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type) + { + _type = type; + } + + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + { + try + { + // Since we are passing BindingFlags.Public, the correct annotation is present. + return Activator.CreateInstance(_type, value); + } + catch (TargetInvocationException ex) + { + if (ex.InnerException == null) + { + throw; + } + + // Rethrow inner exception with original call stack. + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + + // Actually unreachable. + throw; + } + } +} diff --git a/src/Ookii.CommandLine/Conversion/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs new file mode 100644 index 00000000..d51f9a53 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -0,0 +1,134 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// A converter for arguments with enumeration values. +/// +/// +/// +/// 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. +/// +/// +/// 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. +/// +/// +/// If conversion fails, this converter will provide an error message that includes all the +/// allowed values for the enumeration. +/// +/// +/// +public class EnumConverter : ArgumentConverter +{ + /// + /// Initializes a new instance of the for the specified enumeration + /// type. + /// + /// The enumeration type. + /// + /// is . + /// + /// + /// is not an enumeration type. + /// + public EnumConverter(Type enumType) + { + EnumType = enumType ?? throw new ArgumentNullException(nameof(enumType)); + if (!EnumType.IsEnum) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, Properties.Resources.TypeIsNotEnumFormat, EnumType.FullName), + nameof(enumType)); + } + } + + /// + /// Gets the enumeration type that this converter converts to. + /// + /// + /// The enumeration type. + /// + public Type EnumType { get; } + + /// + /// Converts a string to the enumeration type. + /// + /// The string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// + /// This method performs the conversion using the + /// method. + /// + /// + /// + /// 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 + /// + /// Converts a string span to the enumeration type. + /// + /// The containing the string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// + /// This method performs the conversion using the + /// method. + /// + /// + /// + /// The value was not valid for the enumeration type. + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) + { + try + { + return Enum.Parse(EnumType, value, true); + } + catch (ArgumentException ex) + { + throw CreateException(value.ToString(), ex, argument); + } + catch (OverflowException ex) + { + throw CreateException(value.ToString(), ex, argument); + } + } +#endif + + private Exception CreateException(string value, Exception inner, CommandLineArgument argument) + { + var message = argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, EnumType, value, true); + return new CommandLineArgumentException(message, argument.ArgumentName, CommandLineArgumentErrorCategory.ArgumentValueConversion, inner); + } +} diff --git a/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs b/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs new file mode 100644 index 00000000..5bb408ed --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs @@ -0,0 +1,46 @@ +using System; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Sets the namespace to use for argument converters generated for arguments classes with the +/// attribute. +/// +/// +/// +/// To convert argument types for which no built-in non-reflection argument converter exists, +/// such as classes that have a constructor taking a parameter, or those +/// that have a Parse method but don't implement , the source +/// generator will create a new argument converter. The generated converter class will be +/// internal to the assembly containing the generated parser, and will be placed in the namespace +/// Ookii.CommandLine.Conversion.Generated by default. +/// +/// +/// Use this attribute to modify the namespace used. +/// +/// +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class GeneratedConverterNamespaceAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified namespace. + /// + /// The namespace to use. + /// + /// is . + /// + public GeneratedConverterNamespaceAttribute(string @namespace) + { + Namespace = @namespace ?? throw new ArgumentNullException(nameof(@namespace)); + } + + /// + /// Gets the namespace to use for generated argument converters. + /// + /// + /// The full name of the namespace. + /// + public string Namespace { get; } +} diff --git a/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs new file mode 100644 index 00000000..50c8553d --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Specifies a custom to use for the keys of a dictionary argument. +/// +/// +/// +/// This attribute can be used, along with the and +/// attributes, to customize the parsing of a dictionary +/// argument without having to write a custom that returns a +/// . +/// +/// +/// The type specified by this attribute must derive from the +/// class. +/// +/// +/// This attribute is ignored if the argument uses the +/// attribute, or if the argument is not a dictionary argument. +/// +/// +/// +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class KeyConverterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class with the + /// specified converter type. + /// + /// + /// The to use as a converter. + /// + /// + /// is + /// +#if NET6_0_OR_GREATER + public KeyConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type converterType) +#else + public KeyConverterAttribute(Type converterType) +#endif + { + ConverterTypeName = converterType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(converterType)); + } + + /// + /// Initializes a new instance of the class with the + /// specified converter type name. + /// + /// + /// The fully qualified name of the to use as a converter. + /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// + /// + /// is + /// +#if NET6_0_OR_GREATER + public KeyConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] string converterTypeName) +#else + public KeyConverterAttribute(string converterTypeName) +#endif + { + ConverterTypeName = converterTypeName ?? throw new ArgumentNullException(nameof(converterTypeName)); + } + + /// + /// Gets the fully qualified name of the to use as a converter. + /// + /// + /// The fully qualified name of the to use as a converter. + /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + public string ConverterTypeName { get; } + + internal Type GetConverterType() + { + return Type.GetType(ConverterTypeName, true)!; + } +} diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs new file mode 100644 index 00000000..2e231782 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Static class providing constants for the +/// class. +/// +public static class KeyValuePairConverter +{ + /// + /// Gets the default key/value separator, which is "=". + /// + public const string DefaultSeparator = "="; +} + +/// +/// Converts key-value pairs to and from strings using "key=value" notation. +/// +/// The type of the key. +/// The type of the value. +/// +/// +/// This is used for dictionary command line arguments by default. +/// +/// +/// The behavior of this converter can be customized by applying the , +/// or attribute +/// to the property or method defining a dictionary argument. +/// +/// +/// +public class KeyValuePairConverter : ArgumentConverter +{ + /// + /// Initializes a new instance of the class + /// with the specified key and value converters and options. + /// + /// + /// The used to convert the key/value pair's keys. + /// + /// + /// The used to convert the key/value pair's values. + /// + /// + /// An optional custom key/value separator. If , the value + /// of is used. + /// + /// + /// Indicates whether the type of the pair's value accepts values. + /// + /// + /// or is . + /// + /// + /// is an empty string. + /// + public KeyValuePairConverter(ArgumentConverter keyConverter, ArgumentConverter valueConverter, string? separator, bool allowNullValues) + { + AllowNullValues = allowNullValues; + KeyConverter = keyConverter ?? throw new ArgumentNullException(nameof(keyConverter)); + ValueConverter = valueConverter ?? throw new ArgumentNullException(nameof(valueConverter)); + Separator = separator ?? KeyValuePairConverter.DefaultSeparator; + if (Separator.Length == 0) + { + throw new ArgumentException(Properties.Resources.EmptyKeyValueSeparator, nameof(separator)); + } + } + + /// + /// Initializes a new instance of the class. + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Key and value converters cannot be statically determined.")] +#endif + public KeyValuePairConverter() + : this(typeof(TKey).GetStringConverter(null), typeof(TValue).GetStringConverter(null), null, + !typeof(TValue).IsValueType || typeof(TValue).IsNullableValueType()) + { + } + + /// + /// Gets the converter used for the keys of the key/value pair. + /// + /// + /// The used for the keys. + /// + public ArgumentConverter KeyConverter { get; } + + /// + /// Gets the converter used for the values of the key/value pair. + /// + /// + /// The used for the values. + /// + public ArgumentConverter ValueConverter { get; } + + /// + /// Gets the key/value separator. + /// + /// + /// The string used to separate the key and value in a key/value pair. + /// + public string Separator { get; } + + /// + /// Gets a value which indicates whether the values of the key/value pair can be + /// . + /// + /// + /// if values are allowed; otherwise, . + /// + /// + /// + /// This property should only be true if is a value type other + /// than or a reference type without a nullable annotation. + /// + /// + /// The keys of a key/value pair can never be . + /// + /// + public bool AllowNullValues { get; } + + /// + /// Converts a string to a . + /// + /// The string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// or or is + /// . + /// + /// + /// The value was not in a correct format for the target type. + /// + /// + /// The value was not in a correct format for the target type. + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + => Convert((value ?? throw new ArgumentNullException(nameof(value))).AsSpan(), culture, argument); + + /// + /// Converts a string span to a . + /// + /// The containing the string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// An object representing the converted value. + /// + /// or or is + /// . + /// + /// + /// The value was not in a correct format for the target type. + /// + /// + /// The value was not in a correct format for the target type. + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) + { + if (argument == null) + { + throw new ArgumentNullException(nameof(argument)); + } + + var (key, valueForKey) = value.SplitOnce(Separator.AsSpan(), out bool hasSeparator); + if (!hasSeparator) + { + throw new FormatException(argument.Parser.StringProvider.MissingKeyValuePairSeparator(Separator)); + } + + var convertedKey = KeyConverter.Convert(key, culture, argument); + var convertedValue = ValueConverter.Convert(valueForKey, culture, argument); + if (convertedKey == null || !AllowNullValues && convertedValue == null) + { + throw argument.Parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, + argument.ArgumentName); + } + + return new KeyValuePair((TKey)convertedKey, (TValue?)convertedValue); + } +} diff --git a/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs b/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs new file mode 100644 index 00000000..32bf967d --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs @@ -0,0 +1,53 @@ +using System; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Defines a custom key/value separator for dictionary arguments. +/// +/// +/// +/// By default, dictionary arguments use the equals sign ('=') as a separator. By using this +/// attribute, you can choose a custom separator. This separator cannot appear in the key, +/// but can appear in the value. +/// +/// +/// This attribute is ignored if the dictionary argument uses the +/// attribute, or if the argument is not a dictionary argument. +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class KeyValueSeparatorAttribute : Attribute +{ + private readonly string _separator; + + /// + /// Initializes a new instance of the class. + /// + /// The separator to use. + /// is . + /// is an empty string. + public KeyValueSeparatorAttribute(string separator) + { + if (separator == null) + { + throw new ArgumentNullException(nameof(separator)); + } + + if (separator.Length == 0) + { + throw new ArgumentException(Properties.Resources.EmptyKeyValueSeparator, nameof(separator)); + } + + _separator = separator; + } + + /// + /// Gets the separator. + /// + /// + /// The separator. + /// + public string Separator => _separator; +} diff --git a/src/Ookii.CommandLine/Conversion/NullableConverter.cs b/src/Ookii.CommandLine/Conversion/NullableConverter.cs new file mode 100644 index 00000000..b204910b --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/NullableConverter.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Converts from a string to a . +/// +/// +/// +/// This converter uses the specified converter for the type T, except when the input is an +/// empty string, in which case it return . This parallels the behavior +/// of the . +/// +/// +/// +public class NullableConverter : ArgumentConverter +{ + /// + /// Initializes a new instance of the class. + /// + /// The converter to use for the target type. + /// + /// is . + /// + public NullableConverter(ArgumentConverter baseConverter) + { + BaseConverter = baseConverter ?? throw new ArgumentNullException(nameof(baseConverter)); + } + + /// + /// Gets the converter for the underlying type. + /// + /// + /// The for the underlying type. + /// + public ArgumentConverter BaseConverter { get; } + + /// + /// + /// An object representing the converted value, or if the value was an + /// empty string. + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (value.Length == 0) + { + return null; + } + + return BaseConverter.Convert(value, culture, argument); + } + + /// + /// + /// An object representing the converted value, or if the value was an + /// empty string span. + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) + { + if (value.Length == 0) + { + return null; + } + + return BaseConverter.Convert(value, culture, argument); + } +} diff --git a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs new file mode 100644 index 00000000..bf8c39d4 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs @@ -0,0 +1,31 @@ +#if NET7_0_OR_GREATER + +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An argument converter for types that implement the interface. +/// +/// The type to convert to. +/// +/// +/// Conversion is performed using the method. +/// +/// +/// Only use this converter for types that implement the interface, +/// but not the interface. For types that implement the +/// interface, use the +/// class. +/// +/// +/// +public class ParsableConverter : ArgumentConverter + where T : IParsable +{ + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => T.Parse(value, culture); +} + +#endif diff --git a/src/Ookii.CommandLine/Conversion/ParseConverter.cs b/src/Ookii.CommandLine/Conversion/ParseConverter.cs new file mode 100644 index 00000000..ecb8daac --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.Reflection; +using System.Runtime.ExceptionServices; + +namespace Ookii.CommandLine.Conversion; + +internal class ParseConverter : ArgumentConverter +{ + private readonly MethodInfo _method; + private readonly bool _hasCulture; + + public ParseConverter(MethodInfo method, bool hasCulture) + { + _method = method; + _hasCulture = hasCulture; + } + + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + { + var parameters = _hasCulture + ? new object?[] { value, culture } + : new object?[] { value }; + + try + { + return _method.Invoke(null, parameters); + } + catch (TargetInvocationException ex) + { + if (ex.InnerException == null) + { + throw; + } + + // Rethrow inner exception with original call stack. + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + + // Actually unreachable. + throw; + } + } +} diff --git a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs new file mode 100644 index 00000000..10eefd53 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs @@ -0,0 +1,34 @@ +#if NET7_0_OR_GREATER + +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An argument converter for types that implement the interface. +/// +/// The type to convert to. +/// +/// +/// Conversion is performed using the +/// method. +/// +/// +/// For types that implement the interface, but not the +/// interface, use the class. +/// +/// +/// +public class SpanParsableConverter : ArgumentConverter + where T : ISpanParsable +{ + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => T.Parse(value, culture); + + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) + => T.Parse(value, culture); +} + +#endif diff --git a/src/Ookii.CommandLine/Conversion/StringConverter.cs b/src/Ookii.CommandLine/Conversion/StringConverter.cs new file mode 100644 index 00000000..5db9160d --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/StringConverter.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// A converter for arguments with string values. +/// +/// +/// This converter does not perform any actual conversion, and returns the existing string as-is. +/// If the input was a for , a new string is +/// allocated for it. +/// +/// +public class StringConverter : ArgumentConverter +{ + /// + /// A default instance of the converter. + /// + public static readonly StringConverter Instance = new(); + + /// + /// Returns the original string value without modification. + /// + /// The string to convert. + /// The culture to use for the conversion. + /// + /// The that will use the converted value. + /// + /// The value of the parameter. + /// + /// is . + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => value; +} diff --git a/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs new file mode 100644 index 00000000..43568b31 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Specifies a custom to use for the keys of a dictionary argument. +/// +/// +/// +/// This attribute can be used along with the and +/// attributes to customize the parsing of a dictionary +/// argument without having to write a custom that returns a +/// . +/// +/// +/// The type specified by this attribute must derive from the +/// class. +/// +/// +/// This attribute is ignored if the argument uses the +/// or if the argument is not a dictionary argument. +/// +/// +/// +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class ValueConverterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class with the + /// specified converter type. + /// + /// + /// The to use as a converter. + /// + /// + /// is + /// +#if NET6_0_OR_GREATER + public ValueConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type converterType) +#else + public ValueConverterAttribute(Type converterType) +#endif + { + ConverterTypeName = converterType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(converterType)); + } + + /// + /// Initializes a new instance of the class with the + /// specified converter type name. + /// + /// + /// The fully qualified name of the to use as a converter. + /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// + /// + /// is + /// +#if NET6_0_OR_GREATER + public ValueConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] string converterTypeName) +#else + public ValueConverterAttribute(string converterTypeName) +#endif + { + ConverterTypeName = converterTypeName ?? throw new ArgumentNullException(nameof(converterTypeName)); + } + + /// + /// Gets the fully qualified name of the to use as a converter. + /// + /// + /// The fully qualified name of the to use as a converter. + /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + public string ConverterTypeName { get; } + + internal Type GetConverterType() + { + return Type.GetType(ConverterTypeName, true)!; + } +} diff --git a/src/Ookii.CommandLine/Conversion/WrappedDefaultTypeConverter.cs b/src/Ookii.CommandLine/Conversion/WrappedDefaultTypeConverter.cs new file mode 100644 index 00000000..ce7a6ff2 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/WrappedDefaultTypeConverter.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An that wraps the default for a +/// type. +/// +/// The type to convert to. +/// +/// This class will convert argument values from a string using the default +/// for the type . If you wish to use a specific custom , +/// use the class instead. +/// +/// +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Determining the TypeConverter for a type may require the type to be annotated.")] +#endif +public class WrappedDefaultTypeConverter : WrappedTypeConverter +{ + /// + /// Initializes a new instance of the class. + /// + public WrappedDefaultTypeConverter() + : base(TypeDescriptor.GetConverter(typeof(T))) + { + } +} diff --git a/src/Ookii.CommandLine/Conversion/WrappedTypeConverter.cs b/src/Ookii.CommandLine/Conversion/WrappedTypeConverter.cs new file mode 100644 index 00000000..10920000 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/WrappedTypeConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An that wraps an existing . +/// +/// +/// +/// For a convenient way to use to use any with the +/// attribute, use the +/// class. To use the default for a type, use the +/// class. +/// +/// +/// +public class WrappedTypeConverter : ArgumentConverter +{ + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// + /// is . + /// + /// + /// The specified by cannot convert + /// from a . + /// + public WrappedTypeConverter(TypeConverter converter) + { + Converter = converter ?? throw new ArgumentNullException(nameof(converter)); + if (!converter.CanConvertFrom(typeof(string))) + { + throw new ArgumentException(Properties.Resources.InvalidTypeConverter, nameof(converter)); + } + } + + /// + /// Gets the type converter used by this instance. + /// + /// + /// An instance of the class. + /// + public TypeConverter Converter { get; } + + /// + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + => Converter.ConvertFromString(null, culture, value); +} diff --git a/src/Ookii.CommandLine/Conversion/WrappedTypeConverterGeneric.cs b/src/Ookii.CommandLine/Conversion/WrappedTypeConverterGeneric.cs new file mode 100644 index 00000000..d7c1b5fa --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/WrappedTypeConverterGeneric.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An that wraps an existing . +/// +/// The type of the to wrap. +/// +/// +/// This class will convert argument values from a string using the +/// class . If you wish to use the default +/// for a type, use the class instead. +/// +/// +public class WrappedTypeConverter : WrappedTypeConverter + where T : TypeConverter, new() +{ + /// + /// Initializes a new instance of the class. + /// + public WrappedTypeConverter() + : base(new T()) + { + } +} diff --git a/src/Ookii.CommandLine/Convert-SyncMethod.ps1 b/src/Ookii.CommandLine/Convert-SyncMethod.ps1 index 81ce4c83..064b005e 100644 --- a/src/Ookii.CommandLine/Convert-SyncMethod.ps1 +++ b/src/Ookii.CommandLine/Convert-SyncMethod.ps1 @@ -11,7 +11,12 @@ $replacements = @( @("await ", ""), # Remove await keyword @("ReadOnlyMemory", "ReadOnlySpan"), # Async stream functions uses Memory instead of span @(".Span", ""), # Needed to convert Memory usage to Span. - @("async ", "") # Remove keyword from async lambda + @("async ", ""), # Remove keyword from async lambda + @(", CancellationToken cancellationToken = default", ""), # Remove cancellation token parameter + @(", CancellationToken cancellationToken", ""), # Remove cancellation token parameter + @("(CancellationToken cancellationToken)", "()"), # Remove cancellation token parameter + @(", cancellationToken", ""), # Remove cancellation token parameter value + @("(cancellationToken)", "()") # Remove cancellation token parameter value ) $files = Get-Item $Path diff --git a/src/Ookii.CommandLine/DescriptionListFilterMode.cs b/src/Ookii.CommandLine/DescriptionListFilterMode.cs index 827b8264..8b5284aa 100644 --- a/src/Ookii.CommandLine/DescriptionListFilterMode.cs +++ b/src/Ookii.CommandLine/DescriptionListFilterMode.cs @@ -1,27 +1,26 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates which arguments should be included in the description list when generating usage help. +/// +/// +public enum DescriptionListFilterMode { /// - /// Indicates which arguments should be included in the description list when printing usage. + /// Include arguments that have any information that is not included in the syntax, + /// such as aliases, a default value, or a description. /// - /// - public enum DescriptionListFilterMode - { - /// - /// Include arguments that have any information that is not included in the syntax, - /// such as aliases, a default value, or a description. - /// - Information, - /// - /// Include only arguments that have a description. - /// - Description, - /// - /// Include all arguments. - /// - All, - /// - /// Omit the description list entirely. - /// - None - } + Information, + /// + /// Include only arguments that have a description. + /// + Description, + /// + /// Include all arguments. + /// + All, + /// + /// Omit the description list entirely. + /// + None } diff --git a/src/Ookii.CommandLine/DescriptionListSortMode.cs b/src/Ookii.CommandLine/DescriptionListSortMode.cs index ddb95fa1..c32ae5a5 100644 --- a/src/Ookii.CommandLine/DescriptionListSortMode.cs +++ b/src/Ookii.CommandLine/DescriptionListSortMode.cs @@ -1,36 +1,35 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates how the arguments in the description list should be sorted when generating usage help. +/// +/// +public enum DescriptionListSortMode { /// - /// Indicates how the arguments in the description list should be sorted. + /// The descriptions are listed in the same order as the usage syntax: first the positional + /// arguments, then the non-positional required named arguments sorted by name, then the + /// remaining arguments sorted by name. /// - /// - public enum DescriptionListSortMode - { - /// - /// The descriptions are listed in the same order as the usage syntax: first the positional - /// arguments, then the required named arguments sorted by name, then the remaining - /// arguments sorted by name. - /// - UsageOrder, - /// - /// The descriptions are listed in alphabetical order by argument name. If the parsing mode - /// is , this uses the long name of the argument, unless - /// the argument has no long name, in which case the short name is used. - /// - Alphabetical, - /// - /// The same as , but in reverse order. - /// - AlphabeticalDescending, - /// - /// The descriptions are listed in alphabetical order by the short argument name. If the - /// argument has no short name, the long name is used. If the parsing mode is not - /// , this has the same effect as . - /// - AlphabeticalShortName, - /// - /// The same as , but in reverse order. - /// - AlphabeticalShortNameDescending, - } + UsageOrder, + /// + /// The descriptions are listed in alphabetical order by argument name. If the parsing mode + /// is , this uses the long name of the argument, unless + /// the argument has no long name, in which case the short name is used. + /// + Alphabetical, + /// + /// The same as , but in reverse order. + /// + AlphabeticalDescending, + /// + /// The descriptions are listed in alphabetical order by the short argument name. If the + /// argument has no short name, the long name is used. If the parsing mode is not + /// , this has the same effect as . + /// + AlphabeticalShortName, + /// + /// The same as , but in reverse order. + /// + AlphabeticalShortNameDescending, } diff --git a/src/Ookii.CommandLine/DictionaryArgumentInfo.cs b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs new file mode 100644 index 00000000..0456e800 --- /dev/null +++ b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs @@ -0,0 +1,69 @@ +using Ookii.CommandLine.Conversion; +using System; + +namespace Ookii.CommandLine; + +/// +/// Provides information that only applies to dictionary arguments. +/// +/// +public sealed class DictionaryArgumentInfo +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// if duplicate dictionary keys are allowed; otherwise, + /// . + /// + /// The type of the dictionary's keys. + /// The type of the dictionary's values. + /// The separator between the keys and values. + /// + /// or or + /// is . + /// + public DictionaryArgumentInfo(bool allowDuplicateKeys, Type keyType, Type valueType, string keyValueSeparator) + { + AllowDuplicateKeys = allowDuplicateKeys; + KeyType = keyType ?? throw new ArgumentNullException(nameof(keyType)); + ValueType = valueType ?? throw new ArgumentNullException(nameof(valueType)); + KeyValueSeparator = keyValueSeparator ?? throw new ArgumentNullException(nameof(keyValueSeparator)); + } + + /// + /// Gets a value indicating whether this argument allows duplicate keys. + /// + /// + /// if this argument allows duplicate keys; otherwise, . + /// + /// + public bool AllowDuplicateKeys { get; } + + /// + /// Gets the type of the keys of a dictionary argument. + /// + /// + /// The of the keys in the dictionary. + /// + public Type KeyType { get; } + + /// + /// Gets the type of the values of a dictionary argument. + /// + /// + /// The of the values in the dictionary. + /// + public Type ValueType { get; } + + /// + /// Gets the separator for key/value pairs. + /// + /// + /// The custom value specified using the attribute, or + /// if no attribute was + /// present. + /// + /// + public string KeyValueSeparator { get; } +} diff --git a/src/Ookii.CommandLine/DisposableWrapper.cs b/src/Ookii.CommandLine/DisposableWrapper.cs index a7db32a5..f935314d 100644 --- a/src/Ookii.CommandLine/DisposableWrapper.cs +++ b/src/Ookii.CommandLine/DisposableWrapper.cs @@ -1,60 +1,58 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +internal static class DisposableWrapper { - internal static class DisposableWrapper + public static DisposableWrapper Create(T? obj, Func createIfNull) + where T : IDisposable { - public static DisposableWrapper Create(T? obj, Func createIfNull) - where T : IDisposable - { - return new DisposableWrapper(obj, createIfNull); - } + return new DisposableWrapper(obj, createIfNull); } +} - /// - /// Helper to either use an existing instance (and not dispose it), or create an instance - /// and dispose it. - /// - /// - internal class DisposableWrapper : IDisposable - where T : IDisposable - { - private readonly T _inner; - private bool _needDispose; +/// +/// Helper to either use an existing instance (and not dispose it), or create an instance +/// and dispose it. +/// +/// +internal class DisposableWrapper : IDisposable + where T : IDisposable +{ + private readonly T _inner; + private bool _needDispose; - public DisposableWrapper(T? inner, Func createIfNull) + public DisposableWrapper(T? inner, Func createIfNull) + { + if (inner == null) { - if (inner == null) - { - _inner = createIfNull(); - _needDispose = true; - } - else - { - _inner = inner; - } + _inner = createIfNull(); + _needDispose = true; + } + else + { + _inner = inner; } + } - public T Inner => _inner; + public T Inner => _inner; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (_needDispose) { - if (_needDispose) + if (disposing) { - if (disposing) - { - _inner.Dispose(); - } - - _needDispose = false; + _inner.Dispose(); } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _needDispose = false; } } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs index a3ce0438..54ca148d 100644 --- a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs +++ b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs @@ -1,54 +1,76 @@ using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides data for the event. +/// +/// +public class DuplicateArgumentEventArgs : EventArgs { + private readonly CommandLineArgument _argument; + private readonly ReadOnlyMemory _memoryValue; + private readonly string? _stringValue; + private bool _hasValue; + /// - /// Provides data for the event. + /// Initializes a new instance of the class. /// - public class DuplicateArgumentEventArgs : EventArgs + /// The argument that was specified more than once. + /// + /// The raw new value of the argument, or if the argument has no value. + /// + /// + /// is + /// + public DuplicateArgumentEventArgs(CommandLineArgument argument, string? newValue) { - private readonly CommandLineArgument _argument; - private readonly string? _newValue; + _argument = argument ?? throw new ArgumentNullException(nameof(argument)); + _stringValue = newValue; + _hasValue = newValue != null; + } - /// - /// Initializes a new instance of the class. - /// - /// The argument that was specified more than once. - /// The new value of the argument. - /// - /// is - /// - public DuplicateArgumentEventArgs(CommandLineArgument argument, string? newValue) - { - _argument = argument ?? throw new ArgumentNullException(nameof(argument)); - _newValue = newValue; - } + /// + /// Initializes a new instance of the class. + /// + /// The argument that was specified more than once. + /// if the argument has a value; otherwise, . + /// The raw new value of the argument. + /// + /// is + /// + public DuplicateArgumentEventArgs(CommandLineArgument argument, bool hasValue, ReadOnlyMemory newValue) + { + _argument = argument ?? throw new ArgumentNullException(nameof(argument)); + _memoryValue = newValue; + _hasValue = hasValue; + } - /// - /// Gets the argument that was specified more than once. - /// - /// - /// The that was specified more than once. - /// - public CommandLineArgument Argument => _argument; + /// + /// Gets the argument that was specified more than once. + /// + /// + /// The that was specified more than once. + /// + public CommandLineArgument Argument => _argument; - /// - /// Gets the new value that will be assigned to the argument. - /// - /// - /// The raw string value provided on the command line, before conversion. - /// - public string? NewValue => _newValue; + /// + /// Gets the new value that will be assigned to the argument. + /// + /// + /// The raw string value provided on the command line, before conversion, or + /// if the argument is a switch argument that was provided without an explicit value. + /// + public string? NewValue => _hasValue ? (_stringValue ?? _memoryValue.ToString()) : null; - /// - /// Gets or sets a value that indicates whether the value of the argument should stay - /// unmodified. - /// - /// - /// to preserve the current value of the argument, or - /// to replace it with the value of the property. The default value - /// is . - /// - public bool KeepOldValue { get; set; } - } + /// + /// Gets or sets a value that indicates whether the value of the argument should stay + /// unmodified. + /// + /// + /// to preserve the current value of the argument, or + /// to replace it with the value of the property. The default value + /// is . + /// + public bool KeepOldValue { get; set; } } diff --git a/src/Ookii.CommandLine/ErrorMode.cs b/src/Ookii.CommandLine/ErrorMode.cs index 57d7b1f5..f7fcd05d 100644 --- a/src/Ookii.CommandLine/ErrorMode.cs +++ b/src/Ookii.CommandLine/ErrorMode.cs @@ -1,22 +1,21 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates whether something is an error, warning, or allowed. +/// +/// +public enum ErrorMode { /// - /// Indicates whether something is an error, warning, or allowed. + /// The operation should raise an error. /// - /// - public enum ErrorMode - { - /// - /// The operation should raise an error. - /// - Error, - /// - /// The operation should display a warning, but continue. - /// - Warning, - /// - /// The operation should continue silently. - /// - Allow, - } + Error, + /// + /// The operation should display a warning, but continue. + /// + Warning, + /// + /// The operation should continue silently. + /// + Allow, } diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs new file mode 100644 index 00000000..f3114084 --- /dev/null +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -0,0 +1,66 @@ +using Ookii.CommandLine.Commands; +using System; + +namespace Ookii.CommandLine; + +/// +/// Indicates that the target arguments type should use source generation. +/// +/// +/// +/// When this attribute is applied to a class that defines command line arguments, source +/// generation will be used to create a instance for those +/// arguments, instead of the normal approach which uses run-time reflection. +/// +/// +/// To use the generated parser, source generation will add several static methods to the target +/// class: the method, and the +/// method and its overload. If you are targeting an older version of .Net than .Net 7.0, the +/// same methods are added, but they will not implement the static interfaces. +/// +/// +/// Using these generted methods allows trimming your application without warnings, as they avoid the +/// regular constructors of the and +/// class. +/// +/// +/// When using source generation with subcommands, you should also use a class with the +/// attribute to access the commands. +/// +/// +/// +/// Source generation +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class GeneratedParserAttribute : Attribute +{ + /// + /// Gets or sets a value that indicates whether to generate an implementation of the + /// interface for the arguments class. + /// + /// + /// to generate an implementation of the + /// interface; otherwise, . The default value is , + /// but see the remarks. + /// + /// + /// + /// When this property is , the source generator will add static + /// Parse methods to the arguments class which will create a parser and parse the + /// command line arguments in one go. If using .Net 7.0 or later, this will implement + /// the interface on the class. + /// + /// + /// If this property is , only the + /// interface will be implemented. + /// + /// + /// The default behavior is to generate an implementation of the + /// interface methods unless this property is explicitly set to . + /// However, if the class is a subcommand (it implements the interface + /// and has the attribute), the default is to not + /// implement the interface unless this property is explicitly + /// set to . + /// + /// + public bool GenerateParseMethods { get; set; } = true; +} diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs new file mode 100644 index 00000000..f62e6e4c --- /dev/null +++ b/src/Ookii.CommandLine/IParser.cs @@ -0,0 +1,92 @@ +#if NET7_0_OR_GREATER + +using System; + +namespace Ookii.CommandLine; + +/// +/// Defines a mechanism to parse command line arguments into a type. +/// +/// The type that implements this interface. +/// +/// +/// This type is only available when using .Net 7 or later. +/// +/// +/// This interface is automatically implemented on a class when the +/// is used. Classes without that attribute must parse +/// arguments using the +/// method, or create the parser directly by using the +/// constructor; these classes do not support this interface unless it is manually implemented. +/// +/// +/// When using a version of .Net where static interface methods are not supported (versions prior +/// to .Net 7.0), the will still generate the same methods +/// as defined by this interface, just without having them implement the interface. +/// +/// +public interface IParser : IParserProvider + where TSelf : class, IParser +{ + /// + /// Parses the arguments returned by the + /// method using the type , handling errors and showing usage help + /// as required. + /// + /// + /// The options that control parsing behavior and usage help formatting. If + /// , the default options are used. + /// + /// + /// An instance of the type , or if an + /// error occurred or argument parsing was canceled. + /// + /// + /// The cannot use type as the + /// command line arguments type, because it violates one of the rules concerning argument + /// names or positions. Even when the parser was generated using the + /// class, not all those rules can be checked at compile time. + /// + /// + /// + /// This method is typically generated for a class that defines command line arguments by + /// the attribute. + /// + /// + /// + public static abstract TSelf? Parse(ParseOptions? options = null); + + /// + /// Parses the specified command line arguments using the type , + /// handling errors and showing usage help as required. + /// + /// The command line arguments. + /// + /// The options that control parsing behavior and usage help formatting. If + /// , the default options are used. + /// + /// + /// An instance of the type , or if an + /// error occurred or argument parsing was canceled. + /// + /// + /// The cannot use type as the + /// command line arguments type, because it violates one of the rules concerning argument + /// names or positions. Even when the parser was generated using the + /// class, not all those rules can be checked at compile time. + /// + /// + /// + /// This method is typically generated for a class that defines command line arguments by + /// the attribute. + /// + /// + /// + public static abstract TSelf? Parse(string[] args, ParseOptions? options = null); + + /// + /// + public static abstract TSelf? Parse(ReadOnlyMemory args, ParseOptions? options = null); +} + +#endif diff --git a/src/Ookii.CommandLine/IParserProvider.cs b/src/Ookii.CommandLine/IParserProvider.cs new file mode 100644 index 00000000..4f759cf4 --- /dev/null +++ b/src/Ookii.CommandLine/IParserProvider.cs @@ -0,0 +1,57 @@ +#if NET7_0_OR_GREATER + +using System; + +namespace Ookii.CommandLine; + +/// +/// Defines a mechanism to create a for a type. +/// +/// The type that implements this interface. +/// +/// +/// This type is only available when using .Net 7 or later. +/// +/// +/// This interface is automatically implemented on a class when the +/// is used. Classes without that attribute must create +/// the parser directly by using the +/// constructor; these classes do not support this interface unless it is manually implemented. +/// +/// +/// When using a version of .Net where static interface methods are not supported (versions prior +/// to .Net 7.0), the will still generate the same method +/// as defined by this interface, just without having it implement the interface. +/// +/// +public interface IParserProvider + where TSelf : class, IParserProvider +{ + /// + /// Creates a instance using the specified options. + /// + /// + /// The options that control parsing behavior, or to use the + /// default options. + /// + /// + /// An instance of the class for the type + /// . + /// + /// + /// The cannot use type as the + /// command line arguments type, because it violates one of the rules concerning argument + /// names or positions. Even when the parser was generated using the + /// class, not all those rules can be checked at compile time. + /// + /// + /// + /// This method is typically generated for a class that defines command line arguments by + /// the attribute. + /// + /// + /// + public static abstract CommandLineParser CreateParser(ParseOptions? options = null); +} + +#endif diff --git a/src/Ookii.CommandLine/KeyTypeConverterAttribute.cs b/src/Ookii.CommandLine/KeyTypeConverterAttribute.cs deleted file mode 100644 index ee10add4..00000000 --- a/src/Ookii.CommandLine/KeyTypeConverterAttribute.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; -using System.Collections.Generic; -using System.ComponentModel; - -namespace Ookii.CommandLine -{ - /// - /// Specifies a to use for the keys of a dictionary argument. - /// - /// - /// - /// This attribute can be used along with the - /// attribute to customize the parsing of a dictionary argument without having to write a - /// custom that returns a . - /// - /// - /// This attribute is ignored if the argument uses the - /// or if the argument is not a dictionary argument. - /// - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public class KeyTypeConverterAttribute : Attribute - { - private readonly string _converterTypeName; - - /// - /// Initializes a new instance of the class. - /// - /// The type of the custom to use. - /// is . - public KeyTypeConverterAttribute(Type converterType) - { - _converterTypeName = converterType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(converterType)); - } - - /// - /// Initializes a new instance of the class. - /// - /// The type name of the custom to use. - /// is . - public KeyTypeConverterAttribute(string converterTypeName) - { - _converterTypeName = converterTypeName ?? throw new ArgumentNullException(nameof(converterTypeName)); - } - - /// - /// Gets the name of the type of the custom to use. - /// - /// - /// The name of a type derived from the class. - /// - public string ConverterTypeName => _converterTypeName; - } -} diff --git a/src/Ookii.CommandLine/KeyValuePairConverter.cs b/src/Ookii.CommandLine/KeyValuePairConverter.cs deleted file mode 100644 index 2dbcf5a0..00000000 --- a/src/Ookii.CommandLine/KeyValuePairConverter.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; - -namespace Ookii.CommandLine -{ - /// - /// Static class providing constants for the - /// class. - /// - public static class KeyValuePairConverter - { - /// - /// Gets the default key/value separator, which is "=". - /// - public const string DefaultSeparator = "="; - } - - /// - /// Converts key-value pairs to and from strings using "key=value" notation. - /// - /// The type of the key. - /// The type of the value. - /// - /// - /// This is used for dictionary command line arguments by default. - /// - /// - public class KeyValuePairConverter : TypeConverterBase> - { - private readonly TypeConverter _keyConverter; - private readonly TypeConverter _valueConverter; - private readonly string _argumentName; - private readonly bool _allowNullValues; - private readonly string _separator; - private readonly LocalizedStringProvider _stringProvider; - - /// - /// Initializes a new instance of the class. - /// - /// Provides a to get error messages. - /// The name of the argument that this converter is for. - /// Indicates whether the type of the pair's value accepts values. - /// Provides an optional type to use to convert keys. - /// If , the default converter for is used. - /// Provides an optional type to use to convert values. - /// If , the default converter for is used. - /// Provides an optional custom key/value separator. If , the value - /// of is used. - /// or is . - /// is an empty string. - /// Either the key or value does not support converting from a string. - /// - /// - /// If either or is , - /// conversion of those types is done using the rules outlined in the documentation for the - /// method. - /// - /// - public KeyValuePairConverter(LocalizedStringProvider stringProvider, string argumentName, bool allowNullValues, Type? keyConverterType, Type? valueConverterType, string? separator) - { - _stringProvider = stringProvider ?? throw new ArgumentNullException(nameof(stringProvider)); - _argumentName = argumentName ?? throw new ArgumentNullException(nameof(argumentName)); - _allowNullValues = allowNullValues; - _keyConverter = typeof(TKey).GetStringConverter(keyConverterType); - _valueConverter = typeof(TValue).GetStringConverter(valueConverterType); - _separator = separator ?? KeyValuePairConverter.DefaultSeparator; - if (_separator.Length == 0) - { - throw new ArgumentException(Properties.Resources.EmptyKeyValueSeparator, nameof(separator)); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// Either the key or value does not support converting from a string. - public KeyValuePairConverter() - : this(new LocalizedStringProvider(), string.Empty, true, null, null, null) - { - } - - /// - /// - /// Converts from a string to the type of this converter. - /// - /// The could not be converted. - protected override KeyValuePair Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value) - { - var (key, valueForKey) = value.SplitOnce(_separator); - if (valueForKey == null) - { - throw new FormatException(_stringProvider.MissingKeyValuePairSeparator(_separator)); - } - - object? convertedKey = _keyConverter.ConvertFromString(context, culture, key); - object? convertedValue = _valueConverter.ConvertFromString(context, culture, valueForKey); - if (convertedKey == null || (!_allowNullValues && convertedValue == null)) - { - throw _stringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, _argumentName); - } - - return new((TKey)convertedKey, (TValue?)convertedValue); - } - - /// - /// - /// A string representing the object. - /// - protected override string? Convert(ITypeDescriptorContext? context, CultureInfo? culture, KeyValuePair value) - { - var key = _keyConverter.ConvertToString(context, culture, value.Key); - var valueString = _keyConverter.ConvertToString(context, culture, value.Value); - return key + _separator + valueString; - } - } -} diff --git a/src/Ookii.CommandLine/KeyValueSeparatorAttribute.cs b/src/Ookii.CommandLine/KeyValueSeparatorAttribute.cs deleted file mode 100644 index f5d7d4b4..00000000 --- a/src/Ookii.CommandLine/KeyValueSeparatorAttribute.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; - -namespace Ookii.CommandLine -{ - /// - /// Defines a custom key/value separator for dictionary arguments. - /// - /// - /// - /// By default, dictionary arguments use the equals sign ('=') as a separator. By using this - /// attribute, you can choose a custom separator. This separator cannot appear in the key, - /// but can appear in the value. - /// - /// - /// This attribute is ignored if the dictionary argument uses the - /// attribute, or if the argument is not a dictionary argument. - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public class KeyValueSeparatorAttribute : Attribute - { - private readonly string _separator; - - /// - /// Initializes a new instance of the class. - /// - /// The separator to use. - /// is . - /// is an empty string. - public KeyValueSeparatorAttribute(string separator) - { - if (separator == null) - { - throw new ArgumentNullException(nameof(separator)); - } - - if (separator.Length == 0) - { - throw new ArgumentException(Properties.Resources.EmptyKeyValueSeparator, nameof(separator)); - } - - _separator = separator; - } - - /// - /// Gets the separator. - /// - public string Separator => _separator; - } -} diff --git a/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs index d937b5b5..8bb0ede7 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.Async.cs @@ -5,330 +5,337 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +public partial class LineWrappingTextWriter { - public partial class LineWrappingTextWriter + private partial class LineBuffer { + 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) + { + await WriteToAsync(writer, indent, insertNewLine, cancellationToken); + } + } - private partial class LineBuffer + public async Task WriteLineToAsync(TextWriter writer, int indent, CancellationToken cancellationToken) { - public async Task FlushToAsync(TextWriter writer, int indent, bool insertNewLine) + await WriteToAsync(writer, indent, true, 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) { - // Don't use IsContentEmpty because we also want to write if there's only VT sequences. - if (_segments.Count != 0) - { - await WriteToAsync(writer, indent, insertNewLine); - } + await WriteSegmentsAsync(writer, _segments, cancellationToken); } - public async Task WriteLineToAsync(TextWriter writer, int indent) + if (insertNewLine) { - await WriteToAsync(writer, indent, true); + await WriteBlankLineAsync(writer, cancellationToken); } - private async Task WriteToAsync(TextWriter writer, int indent, bool insertNewLine) + ClearCurrentLine(indent); + } + + private async Task WriteSegmentsAsync(TextWriter writer, IEnumerable segments, CancellationToken cancellationToken) + { + await WriteIndentAsync(writer, Indentation); + foreach (var segment in segments) { - // Don't use IsContentEmpty because we also want to write if there's only VT sequences. - if (_segments.Count != 0) + switch (segment.Type) { - await WriteSegmentsAsync(writer, _segments); - } + case StringSegmentType.PartialLineBreak: + case StringSegmentType.LineBreak: + await WriteBlankLineAsync(writer, cancellationToken); + break; - if (insertNewLine) - { - await writer.WriteLineAsync(); + default: + await _buffer.WriteToAsync(writer, segment.Length, cancellationToken); + break; } + } + } - ClearCurrentLine(indent); + public async Task BreakLineAsync(TextWriter writer, ReadOnlyMemory newSegment, int maxLength, int indent, WrappingMode mode, CancellationToken cancellationToken) + { + Debug.Assert(mode != WrappingMode.Disabled); + var forceMode = _hasOverflow ? BreakLineMode.Forward : BreakLineMode.Backward; + var result = await BreakLineAsync(writer, newSegment, maxLength, indent, forceMode, cancellationToken); + if (!result.Success && forceMode != BreakLineMode.Forward) + { + forceMode = mode == WrappingMode.EnabledNoForce ? BreakLineMode.Forward : BreakLineMode.Force; + result = await BreakLineAsync(writer, newSegment, maxLength, indent, forceMode, cancellationToken); } - private async Task WriteSegmentsAsync(TextWriter writer, IEnumerable segments) + _hasOverflow = !result.Success && mode == WrappingMode.EnabledNoForce; + return result; + } + + private async Task BreakLineAsync(TextWriter writer, ReadOnlyMemory newSegment, int maxLength, int indent, BreakLineMode mode, CancellationToken cancellationToken) + { + if (mode == BreakLineMode.Forward) { - await WriteIndentAsync(writer, Indentation); - foreach (var segment in segments) - { - switch (segment.Type) - { - case StringSegmentType.PartialLineBreak: - case StringSegmentType.LineBreak: - await writer.WriteLineAsync(); - break; - - default: - await _buffer.WriteToAsync(writer, segment.Length); - break; - } - } + maxLength = Math.Max(maxLength, LineLength + newSegment.Span.Length - 1); } - public async Task BreakLineAsync(TextWriter writer, ReadOnlyMemory newSegment, int maxLength, int indent, WrappingMode mode) + // Line length can be over the max length if the previous place a line was split + // plus the indentation is more than the line length. + if (LineLength <= maxLength && + newSegment.Span.Length != 0 && + newSegment.BreakLine(maxLength - LineLength, mode, out var splits)) { - Debug.Assert(mode != WrappingMode.Disabled); - var forceMode = _hasOverflow ? BreakLineMode.Forward : BreakLineMode.Backward; - var result = await BreakLineAsync(writer, newSegment, maxLength, indent, forceMode); - if (!result.Success && forceMode != BreakLineMode.Forward) - { - forceMode = mode == WrappingMode.EnabledNoForce ? BreakLineMode.Forward : BreakLineMode.Force; - result = await BreakLineAsync(writer, newSegment, maxLength, indent, forceMode); - } + var (before, after) = splits; + await WriteSegmentsAsync(writer, _segments, cancellationToken); + await before.WriteToAsync(writer, cancellationToken); + await writer.WriteLineAsync(); + ClearCurrentLine(indent); + Indentation = indent; + return new() { Success = true, Remaining = after }; + } - _hasOverflow = !result.Success && mode == WrappingMode.EnabledNoForce; - return result; + // If forward mode is being used, we know there are no usable breaks in the buffer + // because the line would've been broken there before the segment was put in the + // buffer. + if (IsContentEmpty || mode == BreakLineMode.Forward) + { + return new() { Success = false }; } - private async Task BreakLineAsync(TextWriter writer, ReadOnlyMemory newSegment, int maxLength, int indent, BreakLineMode mode) + int offset = 0; + int contentOffset = Indentation; + foreach (var segment in _segments) { - if (mode == BreakLineMode.Forward) - { - maxLength = Math.Max(maxLength, LineLength + newSegment.Span.Length - 1); - } + offset += segment.Length; + contentOffset += segment.ContentLength; + } - // Line length can be over the max length if the previous place a line was split - // plus the indentation is more than the line length. - if (LineLength <= maxLength && - newSegment.Span.Length != 0 && - newSegment.BreakLine(maxLength - LineLength, mode, out var splits)) + for (int segmentIndex = _segments.Count - 1; segmentIndex >= 0; segmentIndex--) + { + var segment = _segments[segmentIndex]; + offset -= segment.Length; + contentOffset -= segment.ContentLength; + if (segment.Type != StringSegmentType.Text || contentOffset > maxLength) { - var (before, after) = splits; - await WriteSegmentsAsync(writer, _segments); - await before.WriteToAsync(writer); - await writer.WriteLineAsync(); - ClearCurrentLine(indent); - Indentation = indent; - return new() { Success = true, Remaining = after }; + continue; } - // If forward mode is being used, we know there are no usable breaks in the buffer - // because the line would've been broken there before the segment was put in the - // buffer. - if (IsContentEmpty || mode == BreakLineMode.Forward) - { - return new() { Success = false }; - } + int breakIndex = mode == BreakLineMode.Force + ? Math.Min(segment.Length, maxLength - contentOffset) + : _buffer.BreakLine(offset, Math.Min(segment.Length, maxLength - contentOffset)); - int offset = 0; - int contentOffset = Indentation; - foreach (var segment in _segments) + if (breakIndex >= 0) { - offset += segment.Length; - contentOffset += segment.ContentLength; - } + await WriteSegmentsAsync(writer, _segments.Take(segmentIndex), cancellationToken); + breakIndex -= offset; + await _buffer.WriteToAsync(writer, breakIndex, cancellationToken); + await writer.WriteLineAsync(); + if (mode != BreakLineMode.Force) + { + _buffer.Discard(1); + breakIndex += 1; + } - for (int segmentIndex = _segments.Count - 1; segmentIndex >= 0; segmentIndex--) - { - var segment = _segments[segmentIndex]; - offset -= segment.Length; - contentOffset -= segment.ContentLength; - if (segment.Type != StringSegmentType.Text || contentOffset > maxLength) + if (breakIndex < segment.Length) { - continue; + _segments.RemoveRange(0, segmentIndex); + segment.Length -= breakIndex; + _segments[0] = segment; + } + else + { + _segments.RemoveRange(0, segmentIndex + 1); } - int breakIndex = mode == BreakLineMode.Force - ? Math.Min(segment.Length, maxLength - contentOffset) - : _buffer.BreakLine(offset, Math.Min(segment.Length, maxLength - contentOffset)); + ContentLength = _segments.Sum(s => s.ContentLength); + Indentation = indent; + return new() { Success = true, Remaining = newSegment }; + } + } - if (breakIndex >= 0) - { - await WriteSegmentsAsync(writer, _segments.Take(segmentIndex)); - breakIndex -= offset; - await _buffer.WriteToAsync(writer, breakIndex); - await writer.WriteLineAsync(); - if (mode != BreakLineMode.Force) - { - _buffer.Discard(1); - breakIndex += 1; - } + return new() { Success = false }; + } + } - if (breakIndex < segment.Length) - { - _segments.RemoveRange(0, segmentIndex); - segment.Length -= breakIndex; - _segments[0] = segment; - } - else - { - _segments.RemoveRange(0, segmentIndex + 1); - } + private async Task FlushCoreAsync(bool insertNewLine, CancellationToken cancellationToken) + { + ThrowIfWriteInProgress(); + if (_lineBuffer != null) + { + await _lineBuffer.FlushToAsync(_baseWriter, insertNewLine ? _indent : 0, insertNewLine, cancellationToken); + } - ContentLength = _segments.Sum(s => s.ContentLength); - Indentation = indent; - return new() { Success = true, Remaining = newSegment }; - } - } + await _baseWriter.FlushAsync(); + } - return new() { Success = false }; + private async Task ResetIndentCoreAsync(CancellationToken cancellationToken) + { + if (_lineBuffer != null) + { + if (!_lineBuffer.IsContentEmpty) + { + await _lineBuffer.FlushToAsync(_baseWriter, 0, true, cancellationToken); + } + else + { + // Leave non-content segments in the buffer. + _lineBuffer.ClearCurrentLine(0, false); } } - - private async Task FlushCoreAsync(bool insertNewLine) + else { - ThrowIfWriteInProgress(); - if (_lineBuffer != null) + if (!_noWrappingState.IndentNextWrite && _noWrappingState.CurrentLineLength > 0) { - await _lineBuffer.FlushToAsync(_baseWriter, insertNewLine ? _indent : 0, insertNewLine); + await _baseWriter.WriteLineAsync(); } - await _baseWriter.FlushAsync(); + _noWrappingState.IndentNextWrite = false; } + } + + private async Task WriteNoMaximumAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + Debug.Assert(Wrapping == WrappingMode.Disabled); - private async Task ResetIndentCoreAsync() + await buffer.SplitAsync(true, async (type, span) => { - if (_lineBuffer != null) + switch (type) { - if (!_lineBuffer.IsContentEmpty) + case StringSegmentType.PartialLineBreak: + // If we already had a partial line break, write it now. + if (_noWrappingState.HasPartialLineBreak) { - await _lineBuffer.FlushToAsync(_baseWriter, 0, true); + await WriteLineBreakDirectAsync(cancellationToken); } else { - // Leave non-content segments in the buffer. - _lineBuffer.ClearCurrentLine(0, false); - } - } - else - { - if (!_noWrappingState.IndentNextWrite && _noWrappingState.CurrentLineLength > 0) - { - await _baseWriter.WriteLineAsync(); + _noWrappingState.HasPartialLineBreak = true; } - _noWrappingState.IndentNextWrite = false; - } - } - - private async Task WriteNoMaximumAsync(ReadOnlyMemory buffer) - { - Debug.Assert(Wrapping == WrappingMode.Disabled); + break; - await buffer.SplitAsync(true, async (type, span) => - { - switch (type) + case StringSegmentType.LineBreak: + // Write an extra line break if there was a partial one and this one isn't the + // end of that line break. + if (_noWrappingState.HasPartialLineBreak) { - case StringSegmentType.PartialLineBreak: - // If we already had a partial line break, write it now. - if (_noWrappingState.HasPartialLineBreak) - { - await WriteLineBreakDirectAsync(); - } - else + _noWrappingState.HasPartialLineBreak = false; + if (span.Span.Length != 1 || span.Span[0] != '\n') { - _noWrappingState.HasPartialLineBreak = true; + await WriteLineBreakDirectAsync(cancellationToken); } + } - break; - - case StringSegmentType.LineBreak: - // Write an extra line break if there was a partial one and this one isn't the - // end of that line break. - if (_noWrappingState.HasPartialLineBreak) - { - _noWrappingState.HasPartialLineBreak = false; - if (span.Span.Length != 1 || span.Span[0] != '\n') - { - await WriteLineBreakDirectAsync(); - } - } + await WriteLineBreakDirectAsync(cancellationToken); + break; - await WriteLineBreakDirectAsync(); - break; + default: + // If we had a partial line break, write it now. + if (_noWrappingState.HasPartialLineBreak) + { + await WriteLineBreakDirectAsync(cancellationToken); + _noWrappingState.HasPartialLineBreak = false; + } - default: - // If we had a partial line break, write it now. - if (_noWrappingState.HasPartialLineBreak) - { - await WriteLineBreakDirectAsync(); - _noWrappingState.HasPartialLineBreak = false; - } + await WriteIndentDirectIfNeededAsync(); + await span.WriteToAsync(_baseWriter, cancellationToken); + _noWrappingState.CurrentLineLength += span.Span.Length; + break; + } + }); + } - await WriteIndentDirectIfNeededAsync(); - await span.WriteToAsync(_baseWriter); - _noWrappingState.CurrentLineLength += span.Span.Length; - break; - } - }); - } + private async Task WriteLineBreakDirectAsync(CancellationToken cancellationToken) + { + await WriteBlankLineAsync(_baseWriter, cancellationToken); + _noWrappingState.IndentNextWrite = _noWrappingState.CurrentLineLength != 0; + _noWrappingState.CurrentLineLength = 0; + } - private async Task WriteLineBreakDirectAsync() + private async Task WriteIndentDirectIfNeededAsync() + { + // Write the indentation if necessary. + if (_noWrappingState.IndentNextWrite) { - await _baseWriter.WriteLineAsync(); - _noWrappingState.IndentNextWrite = _noWrappingState.CurrentLineLength != 0; - _noWrappingState.CurrentLineLength = 0; + await WriteIndentAsync(_baseWriter, _indent); + _noWrappingState.IndentNextWrite = false; } + } - private async Task WriteIndentDirectIfNeededAsync() + private static async Task WriteIndentAsync(TextWriter writer, int indent) + { + for (int x = 0; x < indent; ++x) { - // Write the indentation if necessary. - if (_noWrappingState.IndentNextWrite) - { - await WriteIndentAsync(_baseWriter, _indent); - _noWrappingState.IndentNextWrite = false; - } + await writer.WriteAsync(IndentChar); } + } - private static async Task WriteIndentAsync(TextWriter writer, int indent) + private async Task WriteCoreAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + ThrowIfWriteInProgress(); + if (Wrapping == WrappingMode.Disabled) { - for (int x = 0; x < indent; ++x) - { - await writer.WriteAsync(IndentChar); - } + await WriteNoMaximumAsync(buffer, cancellationToken); + return; } - private async Task WriteCoreAsync(ReadOnlyMemory buffer) + await buffer.SplitAsync(_countFormatting, async (type, span) => { - ThrowIfWriteInProgress(); - if (Wrapping == WrappingMode.Disabled) + // _lineBuffer is guaranteed not null by EnableWrapping but the attribute for that + // only exists in .Net 6.0. + bool hadPartialLineBreak = _lineBuffer!.CheckAndRemovePartialLineBreak(); + if (hadPartialLineBreak) { - await WriteNoMaximumAsync(buffer); - return; + await _lineBuffer.WriteLineToAsync(_baseWriter, _indent, cancellationToken); } - await buffer.SplitAsync(_countFormatting, async (type, span) => + if (type == StringSegmentType.LineBreak) { - // _lineBuffer is guaranteed not null by EnableWrapping but the attribute for that - // only exists in .Net 6.0. - bool hadPartialLineBreak = _lineBuffer!.CheckAndRemovePartialLineBreak(); - if (hadPartialLineBreak) - { - await _lineBuffer.WriteLineToAsync(_baseWriter, _indent); - } - - if (type == StringSegmentType.LineBreak) + // Check if this is just the end of a partial line break. If it is, it was + // already written above. + if (!hadPartialLineBreak || span.Span.Length > 1 || (span.Span.Length == 1 && span.Span[0] != '\n')) { - // Check if this is just the end of a partial line break. If it is, it was - // already written above. - if (!hadPartialLineBreak || span.Span.Length > 1 || (span.Span.Length == 1 && span.Span[0] != '\n')) - { - await _lineBuffer.WriteLineToAsync(_baseWriter, _indent); - } + await _lineBuffer.WriteLineToAsync(_baseWriter, _indent, cancellationToken); } - else + } + else + { + var remaining = span; + if (type == StringSegmentType.Text) { - var remaining = span; - if (type == StringSegmentType.Text) + remaining = _lineBuffer.FindPartialFormattingEnd(remaining); + while (_lineBuffer.LineLength + remaining.Span.Length > _maximumLineLength) { - remaining = _lineBuffer.FindPartialFormattingEnd(remaining); - while (_lineBuffer.LineLength + remaining.Span.Length > _maximumLineLength) + var result = await _lineBuffer.BreakLineAsync(_baseWriter, remaining, _maximumLineLength, _indent, _wrapping, cancellationToken); + if (!result.Success) { - var result = await _lineBuffer.BreakLineAsync(_baseWriter, remaining, _maximumLineLength, _indent, _wrapping); - if (!result.Success) - { - break; - } - - remaining = result.Remaining; + break; } - } - if (remaining.Span.Length > 0) - { - _lineBuffer.Append(remaining.Span, type); - Debug.Assert(_lineBuffer.LineLength <= _maximumLineLength || Wrapping == WrappingMode.EnabledNoForce); + remaining = result.Remaining; } } - }); - } + + if (remaining.Span.Length > 0) + { + _lineBuffer.Append(remaining.Span, type); + Debug.Assert(_lineBuffer.LineLength <= _maximumLineLength || Wrapping == WrappingMode.EnabledNoForce); + } + } + }); + } + private static async Task WriteBlankLineAsync(TextWriter writer, CancellationToken cancellationToken) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await writer.WriteLineAsync(ReadOnlyMemory.Empty, cancellationToken); +#else + await writer.WriteLineAsync(); +#endif } } diff --git a/src/Ookii.CommandLine/LineWrappingTextWriter.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.cs index a62fcd51..d7a27f24 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.cs @@ -1,867 +1,883 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Terminal; +using Ookii.CommandLine.Terminal; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Threading; -using System.ComponentModel; +using System.Threading.Tasks; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Implements a that writes text to another , +/// white-space wrapping lines at the specified maximum line length, and supporting hanging +/// indentation. +/// +/// +/// +/// If the property is not zero, the +/// will buffer the data written to it until an explicit new line is present in the text, or +/// until the length of the buffered data exceeds the value of the +/// property. +/// +/// +/// If the length of the buffered data exceeds the value of the +/// property, the will attempt to find a white-space +/// character to break the line at. If such a white-space character is found, everything +/// before that character is output to the , followed by a line ending, +/// and everything after that character is kept in the buffer. The white-space character +/// itself is not written to the output. +/// +/// +/// If no suitable place to break the line could be found, the line is broken at the maximum +/// line length. This may occur in the middle of a word. If the property +/// is set to , lines without a suitable white-space +/// character will not be wrapped and can be longer than the value of the +/// property. +/// +/// +/// After a line break (either one that was caused by wrapping or one that was part of the +/// text), the next line is indented by the number of characters specified by the +/// property, unless the previous line was blank. The length of the indentation counts towards the +/// maximum line length. +/// +/// +/// When the or method is called, the current +/// contents of the buffer are written to the , followed by a new +/// line, unless the buffer is empty. If the buffer contains only indentation, it is +/// considered empty and no new line is written. Calling has the same +/// effect as writing a new line to the if the buffer is +/// not empty. The is flushed when the +/// or method is called. +/// +/// +/// The or method can be used to move +/// the output position back to the beginning of the line. If the buffer is not empty, is +/// first flushed and indentation is reset to zero on the next line. After the next line +/// break, indentation will again be set to the value of the property. +/// +/// +/// If there is no maximum line length, output is written directly to the +/// and buffering does not occur. Indentation is still inserted as appropriate. +/// +/// +/// The , , and +/// methods will not write an additional new line if the +/// property is zero. +/// +/// +/// +public partial class LineWrappingTextWriter : TextWriter { - /// - /// Implements a that writes text to another , - /// white-space wrapping lines at the specified maximum line length, and supporting indentation. - /// - /// - /// - /// If the property is not zero, the - /// will buffer the data written to it until an explicit new line is present in the text, or - /// until the length of the buffered data exceeds the value of the - /// property. - /// - /// - /// If the length of the buffered data exceeds the value of the - /// property, the will attempt to find a white-space - /// character to break the line at. If such a white-space character is found, everything - /// before that character is output to the , followed by a line ending, - /// and everything after that character is kept in the buffer. The white-space character - /// itself is not written to the output. - /// - /// - /// If no suitable place to break the line could be found, the line is broken at the maximum - /// line length. This may occur in the middle of a word. - /// - /// - /// After a line break (either one that was caused by wrapping or one that was part of the - /// text), the next line is indented by the number of characters specified by the - /// property. The length of the indentation counts towards the maximum line length. - /// - /// - /// When the or method is called, the current - /// contents of the buffer are written to the , followed by a new - /// line, unless the buffer is empty. If the buffer contains only indentation, it is - /// considered empty and no new line is written. Calling has the same - /// effect as writing a new line to the if the buffer is - /// not empty. The is flushed when the - /// or method is called. - /// - /// - /// The or method can be used to move - /// the output position back to the beginning of the line. If the buffer is not empty, is - /// first flushed and indentation is reset to zero on the next line. After the next line - /// break, indentation will again be set to the value of the property. - /// - /// - /// If there is no maximum line length, output is written directly to the - /// and buffering does not occur. Indentation is still inserted as appropriate. - /// - /// - /// The , , and - /// methods will not write an additional new line if the - /// property is zero. - /// - /// - /// - public partial class LineWrappingTextWriter : TextWriter - { - #region Nested types + #region Nested types - [DebuggerDisplay("Type = {Type}, ContentLength = {ContentLength}, Length = {Length}")] - private struct Segment + [DebuggerDisplay("Type = {Type}, ContentLength = {ContentLength}, Length = {Length}")] + private struct Segment + { + public Segment(StringSegmentType type, int length) { - public Segment(StringSegmentType type, int length) - { - Type = type; - Length = length; - } + Type = type; + Length = length; + } - public StringSegmentType Type { get; set; } - public int Length { get; set; } + public StringSegmentType Type { get; set; } + public int Length { get; set; } - public int ContentLength => IsContent(Type) ? Length : 0; + public int ContentLength => IsContent(Type) ? Length : 0; - public static bool IsContent(StringSegmentType type) - => type <= StringSegmentType.LineBreak; + public static bool IsContent(StringSegmentType type) + => type <= StringSegmentType.LineBreak; - } + } - private struct AsyncBreakLineResult - { - public bool Success { get; set; } - public ReadOnlyMemory Remaining { get; set; } - } + private struct AsyncBreakLineResult + { + public bool Success { get; set; } + public ReadOnlyMemory Remaining { get; set; } + } - private ref struct BreakLineResult - { - public bool Success { get; set; } - public ReadOnlySpan Remaining { get; set; } - } + private ref struct BreakLineResult + { + public bool Success { get; set; } + public ReadOnlySpan Remaining { get; set; } + } + + private partial class LineBuffer + { + private readonly RingBuffer _buffer; + private readonly List _segments = new(); + private bool _hasOverflow; - private partial class LineBuffer + public LineBuffer(int capacity) { - private readonly RingBuffer _buffer; - private readonly List _segments = new(); - private bool _hasOverflow; + _buffer = new(capacity); + } - public LineBuffer(int capacity) - { - _buffer = new(capacity); - } + public int ContentLength { get; private set; } - public int ContentLength { get; private set; } + public bool IsContentEmpty => ContentLength == 0; - public bool IsContentEmpty => ContentLength == 0; + public bool IsEmpty => _segments.Count == 0; - public bool IsEmpty => _segments.Count == 0; + public int Indentation { get; set; } - public int Indentation { get; set; } + public int LineLength => ContentLength + Indentation; - public int LineLength => ContentLength + Indentation; + public void Append(ReadOnlySpan span, StringSegmentType type) + { + Debug.Assert(type != StringSegmentType.LineBreak); - public void Append(ReadOnlySpan span, StringSegmentType type) + // If we got here, we know the line length is not overflowing, so copy everything + // except partial linebreaks into the buffer. + if (type != StringSegmentType.PartialLineBreak) { - Debug.Assert(type != StringSegmentType.LineBreak); - - // If we got here, we know the line length is not overflowing, so copy everything - // except partial linebreaks into the buffer. - if (type != StringSegmentType.PartialLineBreak) - { - _buffer.CopyFrom(span); - } + _buffer.CopyFrom(span); + } - if (LastSegment is Segment last) + if (LastSegment is Segment last) + { + if (last.Type == type) { - if (last.Type == type) + last.Length += span.Length; + _segments[_segments.Count - 1] = last; + if (Segment.IsContent(type)) { - last.Length += span.Length; - _segments[_segments.Count - 1] = last; - if (Segment.IsContent(type)) - { - ContentLength += span.Length; - } - - return; + ContentLength += span.Length; } - else if (last.Type >= StringSegmentType.PartialFormattingUnknown) - { - Debug.Assert(type != StringSegmentType.Text); - // If this is not a text segment, we never found the end of the formatting, - // so just treat everything up to now as formatting. - last.Type = StringSegmentType.Formatting; - _segments[_segments.Count - 1] = last; - } + return; } + else if (last.Type >= StringSegmentType.PartialFormattingUnknown) + { + Debug.Assert(type != StringSegmentType.Text); - var segment = new Segment(type, span.Length); - _segments.Add(segment); - var contentLength = segment.ContentLength; - ContentLength += contentLength; + // If this is not a text segment, we never found the end of the formatting, + // so just treat everything up to now as formatting. + last.Type = StringSegmentType.Formatting; + _segments[_segments.Count - 1] = last; + } } - public Segment? LastSegment => _segments.Count > 0 ? _segments[_segments.Count - 1] : null; + var segment = new Segment(type, span.Length); + _segments.Add(segment); + var contentLength = segment.ContentLength; + ContentLength += contentLength; + } - public bool HasPartialFormatting => LastSegment is Segment last && last.Type >= StringSegmentType.PartialFormattingUnknown; + public Segment? LastSegment => _segments.Count > 0 ? _segments[_segments.Count - 1] : null; - public partial void FlushTo(TextWriter writer, int indent, bool insertNewLine); + public bool HasPartialFormatting => LastSegment is Segment last && last.Type >= StringSegmentType.PartialFormattingUnknown; - public partial void WriteLineTo(TextWriter writer, int indent); + public partial void FlushTo(TextWriter writer, int indent, bool insertNewLine); - public void Peek(TextWriter writer) - { - WriteIndent(writer, Indentation); - int offset = 0; - foreach (var segment in _segments) - { - switch (segment.Type) - { - case StringSegmentType.PartialLineBreak: - case StringSegmentType.LineBreak: - writer.WriteLine(); - break; - - default: - _buffer.Peek(writer, offset, segment.Length); - offset += segment.Length; - break; - } - } - } + public partial void WriteLineTo(TextWriter writer, int indent); - public bool CheckAndRemovePartialLineBreak() + public void Peek(TextWriter writer) + { + WriteIndent(writer, Indentation); + int offset = 0; + foreach (var segment in _segments) { - if (LastSegment is Segment last && last.Type == StringSegmentType.PartialLineBreak) + switch (segment.Type) { - _segments.RemoveAt(_segments.Count - 1); - return true; + case StringSegmentType.PartialLineBreak: + case StringSegmentType.LineBreak: + writer.WriteLine(); + break; + + default: + _buffer.Peek(writer, offset, segment.Length); + offset += segment.Length; + break; } - - return false; } + } - public ReadOnlySpan FindPartialFormattingEnd(ReadOnlySpan newSegment) + public bool CheckAndRemovePartialLineBreak() + { + if (LastSegment is Segment last && last.Type == StringSegmentType.PartialLineBreak) { - return newSegment.Slice(FindPartialFormattingEndCore(newSegment)); + _segments.RemoveAt(_segments.Count - 1); + return true; } - public ReadOnlyMemory FindPartialFormattingEnd(ReadOnlyMemory newSegment) - { - return newSegment.Slice(FindPartialFormattingEndCore(newSegment.Span)); - } + return false; + } - private partial void WriteTo(TextWriter writer, int indent, bool insertNewLine); + public ReadOnlySpan FindPartialFormattingEnd(ReadOnlySpan newSegment) + { + return newSegment.Slice(FindPartialFormattingEndCore(newSegment)); + } - private int FindPartialFormattingEndCore(ReadOnlySpan newSegment) - { - if (LastSegment is not Segment lastSegment || lastSegment.Type < StringSegmentType.PartialFormattingUnknown) - { - // There is no partial formatting. - return 0; - } + public ReadOnlyMemory FindPartialFormattingEnd(ReadOnlyMemory newSegment) + { + return newSegment.Slice(FindPartialFormattingEndCore(newSegment.Span)); + } - var type = lastSegment.Type; - int index = VirtualTerminal.FindSequenceEnd(newSegment, ref type); - if (index < 0) - { - // No ending found, concatenate this to the last segment. - _buffer.CopyFrom(newSegment); - lastSegment.Length += newSegment.Length; - lastSegment.Type = type; - _segments[_segments.Count - 1] = lastSegment; - return newSegment.Length; - } + private partial void WriteTo(TextWriter writer, int indent, bool insertNewLine); - // Concatenate the rest of the formatting. - index += 1; - _buffer.CopyFrom(newSegment.Slice(0, index)); - lastSegment.Length += index; - lastSegment.Type = StringSegmentType.Formatting; - _segments[_segments.Count - 1] = lastSegment; - return index; + private int FindPartialFormattingEndCore(ReadOnlySpan newSegment) + { + if (LastSegment is not Segment lastSegment || lastSegment.Type < StringSegmentType.PartialFormattingUnknown) + { + // There is no partial formatting. + return 0; } - private partial void WriteSegments(TextWriter writer, IEnumerable segments); + var type = lastSegment.Type; + int index = VirtualTerminal.FindSequenceEnd(newSegment, ref type); + if (index < 0) + { + // No ending found, concatenate this to the last segment. + _buffer.CopyFrom(newSegment); + lastSegment.Length += newSegment.Length; + lastSegment.Type = type; + _segments[_segments.Count - 1] = lastSegment; + return newSegment.Length; + } - public partial BreakLineResult BreakLine(TextWriter writer, ReadOnlySpan newSegment, int maxLength, int indent, WrappingMode mode); + // Concatenate the rest of the formatting. + index += 1; + _buffer.CopyFrom(newSegment.Slice(0, index)); + lastSegment.Length += index; + lastSegment.Type = StringSegmentType.Formatting; + _segments[_segments.Count - 1] = lastSegment; + return index; + } - private partial BreakLineResult BreakLine(TextWriter writer, ReadOnlySpan newSegment, int maxLength, int indent, BreakLineMode mode); + private partial void WriteSegments(TextWriter writer, IEnumerable segments); - public void ClearCurrentLine(int indent, bool clearSegments = true) - { - if (clearSegments) - { - _segments.Clear(); - } + public partial BreakLineResult BreakLine(TextWriter writer, ReadOnlySpan newSegment, int maxLength, int indent, WrappingMode mode); - if (!IsContentEmpty) - { - Indentation = indent; - } - else - { - Indentation = 0; - } + private partial BreakLineResult BreakLine(TextWriter writer, ReadOnlySpan newSegment, int maxLength, int indent, BreakLineMode mode); - ContentLength = 0; + public void ClearCurrentLine(int indent, bool clearSegments = true) + { + if (clearSegments) + { + _segments.Clear(); } - } - struct NoWrappingState - { - public int CurrentLineLength { get; set; } - public bool IndentNextWrite { get; set; } - public bool HasPartialLineBreak { get; set; } - } - -#endregion - - private const char IndentChar = ' '; - - private readonly TextWriter _baseWriter; - private readonly LineBuffer? _lineBuffer; - private readonly bool _disposeBaseWriter; - private readonly int _maximumLineLength; - private readonly bool _countFormatting; - private int _indent; - private WrappingMode _wrapping = WrappingMode.Enabled; - - // Used for indenting when there is no maximum line length. - private NoWrappingState _noWrappingState; - - // Used to discourage calling sync methods when an async method is in progress on the same - // thread. - private Task _asyncWriteTask = Task.CompletedTask; - - /// - /// Initializes a new instance of the class. - /// - /// The to which to write the wrapped output. - /// The maximum length of a line, in characters; a value of less than 1 or larger than 65536 means there is no maximum line length. - /// If set to the will be disposed when the is disposed. - /// - /// If set to , virtual terminal sequences used to format the text - /// will not be counted as part of the line length, and will therefore not affect where - /// the text is wrapped. The default value is . - /// - /// - /// is . - /// - /// - /// - /// The largest value supported is 65535. Above that, line length is considered to be unbounded. This is done - /// to avoid having to buffer large amounts of data to support these long line lengths. - /// - /// - /// If you want to write to the console, use or as the and - /// specify - 1 as the and for . If you don't - /// subtract one from the window width, additional empty lines can be printed if a line is exactly the width of the console. You can easily create a - /// that writes to the console by using the and methods. - /// - /// - public LineWrappingTextWriter(TextWriter baseWriter, int maximumLineLength, bool disposeBaseWriter = true, bool countFormatting = false) - : base(baseWriter?.FormatProvider) - { - _baseWriter = baseWriter ?? throw new ArgumentNullException(nameof(baseWriter)); - base.NewLine = baseWriter.NewLine; - // We interpret anything larger than 65535 to mean infinite length to avoid buffering that much. - _maximumLineLength = (maximumLineLength is < 1 or > ushort.MaxValue) ? 0 : maximumLineLength; - _disposeBaseWriter = disposeBaseWriter; - _countFormatting = countFormatting; - if (_maximumLineLength > 0) + if (!IsContentEmpty) { - // Add some slack for formatting characters. - _lineBuffer = new(countFormatting ? _maximumLineLength : _maximumLineLength * 2); + Indentation = indent; } + else + { + Indentation = 0; + } + + ContentLength = 0; } + } + + struct NoWrappingState + { + public int CurrentLineLength { get; set; } + public bool IndentNextWrite { get; set; } + public bool HasPartialLineBreak { get; set; } + } + #endregion - /// - /// Gets the that this is writing to. - /// - /// - /// The that this is writing to. - /// - public TextWriter BaseWriter - { - get { return _baseWriter; } - } + private const char IndentChar = ' '; - /// - public override Encoding Encoding + private readonly TextWriter _baseWriter; + private readonly LineBuffer? _lineBuffer; + private readonly bool _disposeBaseWriter; + private readonly int _maximumLineLength; + private readonly bool _countFormatting; + private int _indent; + private WrappingMode _wrapping = WrappingMode.Enabled; + + // Used for indenting when there is no maximum line length. + private NoWrappingState _noWrappingState; + + // Used to discourage calling sync methods when an async method is in progress on the same + // thread. + private Task _asyncWriteTask = Task.CompletedTask; + + /// + /// Initializes a new instance of the class. + /// + /// The to which to write the wrapped output. + /// The maximum length of a line, in characters; a value of less than 1 or larger than 65536 means there is no maximum line length. + /// If set to the will be disposed when the is disposed. + /// + /// If set to , virtual terminal sequences used to format the text + /// will not be counted as part of the line length, and will therefore not affect where + /// the text is wrapped. The default value is . + /// + /// + /// is . + /// + /// + /// + /// The largest value supported is 65535. Above that, line length is considered to be unbounded. This is done + /// to avoid having to buffer large amounts of data to support these long line lengths. + /// + /// + /// If you want to write to the console, use or as the and + /// specify - 1 as the and for . If you don't + /// subtract one from the window width, additional empty lines can be printed if a line is exactly the width of the console. You can easily create a + /// that writes to the console by using the and methods. + /// + /// + public LineWrappingTextWriter(TextWriter baseWriter, int maximumLineLength, bool disposeBaseWriter = true, bool countFormatting = false) + : base(baseWriter?.FormatProvider) + { + _baseWriter = baseWriter ?? throw new ArgumentNullException(nameof(baseWriter)); + base.NewLine = baseWriter.NewLine; + // We interpret anything larger than 65535 to mean infinite length to avoid buffering that much. + _maximumLineLength = (maximumLineLength is < 1 or > ushort.MaxValue) ? 0 : maximumLineLength; + _disposeBaseWriter = disposeBaseWriter; + _countFormatting = countFormatting; + if (_maximumLineLength > 0) { - get { return _baseWriter.Encoding; } + // Add some slack for formatting characters. + _lineBuffer = new(countFormatting ? _maximumLineLength : _maximumLineLength * 2); } + } + - /// + /// + /// Gets the that this is writing to. + /// + /// + /// The that this is writing to. + /// + public TextWriter BaseWriter + { + get { return _baseWriter; } + } + + /// + /// Gets the character encoding in which the output is written. + /// + /// + /// The character encoding of the . + /// + public override Encoding Encoding => _baseWriter.Encoding; + + /// + /// + /// The line terminator string use by the . + /// #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - [AllowNull] + [AllowNull] #endif - public override string NewLine + public override string NewLine + { + get => _baseWriter.NewLine; + set + { + base.NewLine = value; + _baseWriter.NewLine = value; + } + } + + /// + /// Gets the maximum length of a line in the output. + /// + /// + /// The maximum length of a line, or zero if the line length is not limited. + /// + public int MaximumLineLength + { + get { return _maximumLineLength; } + } + + /// + /// Gets or sets the amount of characters to indent all but the first line. + /// + /// + /// The amount of characters to indent all but the first line of text. + /// + /// + /// + /// 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. + /// + /// + /// The output position can be reset to the start of the line after a line break by calling + /// the method. + /// + /// + public int Indent + { + get { return _indent; } + set { - get => _baseWriter.NewLine; - set + if (value < 0 || (_maximumLineLength > 0 && value >= _maximumLineLength)) { - base.NewLine = value; - _baseWriter.NewLine = value; + throw new ArgumentOutOfRangeException(nameof(value), Properties.Resources.IndentOutOfRange); } + + _indent = value; } + } - /// - /// Gets the maximum length of a line in the output. - /// - /// - /// The maximum length of a line, or zero if the line length is not limited. - /// - public int MaximumLineLength - { - get { return _maximumLineLength; } - } - - /// - /// Gets or sets the amount of characters to indent all but the first line. - /// - /// - /// The amount of characters to indent all but the first line of text. - /// - /// - /// - /// 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 the property. - /// - /// - /// The output position can be reset to the start of the line after a line break by calling . - /// - /// - public int Indent - { - get { return _indent; } - set + /// + /// Gets or sets a value which indicates how to wrap lines at the maximum line length. + /// + /// + /// One of the values of the enumeration. If no maximum line + /// length is set, the value is always . + /// + /// + /// + /// When this property is changed to the buffer will + /// be flushed synchronously if not empty. + /// + /// + /// When this property is changed from to another + /// value, if the last character written was not a new line, the current line may not be + /// correctly wrapped. + /// + /// + /// Changing this property resets indentation so the next write will not be indented. + /// + /// + /// This property cannot be changed if there is no maximum line length. + /// + /// + public WrappingMode Wrapping + { + get => _lineBuffer != null ? _wrapping : WrappingMode.Disabled; + set + { + ThrowIfWriteInProgress(); + if (_lineBuffer != null && _wrapping != value) { - if (value < 0 || (_maximumLineLength > 0 && value >= _maximumLineLength)) + if (value == WrappingMode.Disabled) { - throw new ArgumentOutOfRangeException(nameof(value), Properties.Resources.IndentOutOfRange); + // 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); + + // Ensure no state is carried over from the last time this was changed. + _noWrappingState = default; } - _indent = value; + _wrapping = value; } } + } - /// - /// Gets or sets a value which indicates how to wrap lines at the maximum line length. - /// - /// - /// One of the values of the enumeration. If no maximum line - /// length is set, the value is always . - /// - /// - /// - /// When this property is changed to the buffer will - /// be flushed synchronously if not empty. - /// - /// - /// When this property is changed from to another - /// value, if the last character written was not a new line, the current line may not be - /// correctly wrapped. - /// - /// - /// Changing this property resets indentation so the next write will not be indented. - /// - /// - /// This property cannot be changed if there is no maximum line length. - /// - /// - public WrappingMode Wrapping - { - get => _lineBuffer != null ? _wrapping : WrappingMode.Disabled; - set - { - ThrowIfWriteInProgress(); - if (_lineBuffer != null && _wrapping != value) - { - if (value == WrappingMode.Disabled) - { - // 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); + /// + /// Gets a that writes to the standard output stream, + /// using as the maximum line length. + /// + /// + /// A that writes to , + /// the standard output stream. + /// + public static LineWrappingTextWriter ForConsoleOut() + { + return new LineWrappingTextWriter(Console.Out, GetLineLengthForConsole(), false); + } - // Ensure no state is carried over from the last time this was changed. - _noWrappingState = default; - } + /// + /// Gets a that writes to the standard error stream, + /// using as the maximum line length. + /// + /// + /// A that writes to , + /// the standard error stream. + /// + public static LineWrappingTextWriter ForConsoleError() + { + return new LineWrappingTextWriter(Console.Error, GetLineLengthForConsole(), false); + } - _wrapping = value; - } - } - } + /// + /// Gets a that writes to a . + /// + /// + /// The maximum length of a line, in characters, or 0 to use no maximum. + /// + /// An that controls formatting. + /// + /// If set to , virtual terminal sequences used to format the text + /// will not be counted as part of the line length, and will therefore not affect where + /// the text is wrapped. The default value is . + /// + /// A that writes to a . + /// + /// 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); + } - /// - /// Gets a that writes to the standard output stream, - /// using as the maximum line length. - /// - /// A that writes to the standard output stream. - public static LineWrappingTextWriter ForConsoleOut() + /// + public override void Write(char value) + { + unsafe { - return new LineWrappingTextWriter(Console.Out, GetLineLengthForConsole(), false); + WriteCore(new ReadOnlySpan(&value, 1)); } + } - /// - /// Gets a that writes to the standard error stream, - /// using as the maximum line length. - /// - /// A that writes to the standard error stream. - public static LineWrappingTextWriter ForConsoleError() + /// + public override void Write(string? value) + { + if (value != null) { - return new LineWrappingTextWriter(Console.Error, GetLineLengthForConsole(), false); + WriteCore(value.AsSpan()); } + } - /// - /// Gets a that writes to a . - /// - /// - /// The maximum length of a line, in characters, or 0 to use no maximum. - /// - /// An that controls formatting. - /// - /// If set to , virtual terminal sequences used to format the text - /// will not be counted as part of the line length, and will therefore not affect where - /// the text is wrapped. The default value is . - /// - /// A that writes to a . - /// - /// To retrieve the resulting string, first call , then use the method of the . - /// - public static LineWrappingTextWriter ForStringWriter(int maximumLineLength = 0, IFormatProvider? formatProvider = null, bool countFormatting = false) + /// + public override void Write(char[] buffer, int index, int count) + { + if (buffer == null) { - return new LineWrappingTextWriter(new StringWriter(formatProvider), maximumLineLength, true, countFormatting); + throw new ArgumentNullException(nameof(buffer)); } - /// - public override void Write(char value) + if (index < 0) { - unsafe - { - WriteCore(new ReadOnlySpan(&value, 1)); - } + throw new ArgumentOutOfRangeException(nameof(index), Properties.Resources.ValueMustBeNonNegative); } - /// - public override void Write(string? value) + if (count < 0) { - if (value != null) - { - WriteCore(value.AsSpan()); - } + throw new ArgumentOutOfRangeException(nameof(count), Properties.Resources.ValueMustBeNonNegative); } - /// - public override void Write(char[] buffer, int index, int count) + if ((buffer.Length - index) < count) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), Properties.Resources.ValueMustBeNonNegative); - } + throw new ArgumentException(Properties.Resources.IndexCountOutOfRange); + } - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), Properties.Resources.ValueMustBeNonNegative); - } + WriteCore(new ReadOnlySpan(buffer, index, count)); + } - if ((buffer.Length - index) < count) - { - throw new ArgumentException(Properties.Resources.IndexCountOutOfRange); - } + /// + public override Task WriteAsync(char value) + { + // Array creation is unavoidable here because ReadOnlyMemory can't use a pointer. + var task = WriteCoreAsync(new[] { value }); + _asyncWriteTask = task; + return task; + } - WriteCore(new ReadOnlySpan(buffer, index, count)); + /// + public override Task WriteAsync(string? value) + { + if (value == null) + { + return Task.CompletedTask; } - /// - public override Task WriteAsync(char value) + var task = WriteCoreAsync(value.AsMemory()); + _asyncWriteTask = task; + return task; + } + + /// + public override Task WriteAsync(char[] buffer, int index, int count) + { + if (buffer == null) { - // Array creation is unavoidable here because ReadOnlyMemory can't use a pointer. - var task = WriteCoreAsync(new[] { value }); - _asyncWriteTask = task; - return task; + throw new ArgumentNullException(nameof(buffer)); } - /// - public override Task WriteAsync(string? value) + if (index < 0) { - if (value == null) - { - return Task.CompletedTask; - } - - var task = WriteCoreAsync(value.AsMemory()); - _asyncWriteTask = task; - return task; + throw new ArgumentOutOfRangeException(nameof(index), Properties.Resources.ValueMustBeNonNegative); } - /// - public override Task WriteAsync(char[] buffer, int index, int count) + if (count < 0) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } + throw new ArgumentOutOfRangeException(nameof(count), Properties.Resources.ValueMustBeNonNegative); + } - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), Properties.Resources.ValueMustBeNonNegative); - } + if ((buffer.Length - index) < count) + { + throw new ArgumentException(Properties.Resources.IndexCountOutOfRange); + } - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), Properties.Resources.ValueMustBeNonNegative); - } + var task = WriteCoreAsync(new ReadOnlyMemory(buffer, index, count)); + _asyncWriteTask = task; + return task; + } - if ((buffer.Length - index) < count) - { - throw new ArgumentException(Properties.Resources.IndexCountOutOfRange); - } + /// + public override async Task WriteLineAsync() => await WriteAsync(CoreNewLine); - var task = WriteCoreAsync(new ReadOnlyMemory(buffer, index, count)); - _asyncWriteTask = task; - return task; - } + /// + public override async Task WriteLineAsync(char value) + { + await WriteAsync(value); + await WriteLineAsync(); + } - /// - public override async Task WriteLineAsync() => await WriteAsync(CoreNewLine); + /// + public override async Task WriteLineAsync(char[] buffer, int index, int count) + { + await WriteAsync(buffer, index, count); + await WriteLineAsync(); + } - /// - public override async Task WriteLineAsync(char value) + /// + public override async Task WriteLineAsync(string? value) + { + if (value != null) { await WriteAsync(value); - await WriteLineAsync(); } - /// - public override async Task WriteLineAsync(char[] buffer, int index, int count) - { - await WriteAsync(buffer, index, count); - await WriteLineAsync(); - } - - /// - public override async Task WriteLineAsync(string? value) - { - if (value != null) - { - await WriteAsync(value); - } - - await WriteLineAsync(); - } + await WriteLineAsync(); + } #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - /// - public override void Write(ReadOnlySpan buffer) => WriteCore(buffer); + /// + public override void Write(ReadOnlySpan buffer) => WriteCore(buffer); - /// - public override void WriteLine(ReadOnlySpan buffer) - { - Write(buffer); - WriteLine(); - } + /// + public override void WriteLine(ReadOnlySpan buffer) + { + Write(buffer); + WriteLine(); + } - /// - public override async ValueTask DisposeAsync() + /// + public override async ValueTask DisposeAsync() + { + await FlushAsync(); + await base.DisposeAsync(); + if (_disposeBaseWriter) { - await FlushAsync(); - await base.DisposeAsync(); - if (_disposeBaseWriter) - { - await _baseWriter.DisposeAsync(); - } - - GC.SuppressFinalize(this); + await _baseWriter.DisposeAsync(); } - /// - public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - // TODO: Use cancellation token if possible. - _asyncWriteTask = WriteCoreAsync(buffer); - return _asyncWriteTask; - } + GC.SuppressFinalize(this); + } - /// - public override async Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + /// + public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) { - await WriteAsync(buffer, cancellationToken); - await WriteAsync(CoreNewLine.AsMemory(), cancellationToken); + return Task.FromCanceled(cancellationToken); } + _asyncWriteTask = WriteCoreAsync(buffer, cancellationToken); + return _asyncWriteTask; + } + + /// + public override async Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + await WriteAsync(buffer, cancellationToken); + await WriteAsync(CoreNewLine.AsMemory(), cancellationToken); + } + #endif - /// - public override void Flush() => Flush(true); - - /// - public override Task FlushAsync() => FlushAsync(true); - - /// - /// Clears all buffers for this and causes any buffered data to be - /// written to the underlying writer, optionally inserting an additional new line. - /// - /// - /// Insert an additional new line if the line buffer is not empty. This has no effect if - /// the line buffer is empty or the property is zero. - /// - /// - /// - /// If is set to , the - /// class will not know the length of the flushed - /// line, and therefore the current line may not be correctly wrapped if more text is - /// written to the . - /// - /// - /// For this reason, it's recommended to only set to - /// if you are done writing to this instance. - /// - /// - /// Indentation is reset by this method, so the next write after calling flush will not - /// be indented. - /// - /// - /// The method is equivalent to calling this method with - /// set to . - /// - /// - public void Flush(bool insertNewLine) => FlushCore(insertNewLine); - - /// - /// Clears all buffers for this and causes any buffered data to be - /// written to the underlying writer, optionally inserting an additional new line. - /// - /// - /// Insert an additional new line if the line buffer is not empty. This has no effect if - /// the line buffer is empty or the property is zero. - /// - /// A task that represents the asynchronous flush operation. - /// - /// - /// If is set to , the - /// class will not know the length of the flushed - /// line, and therefore the current line may not be correctly wrapped if more text is - /// written to the . - /// - /// - /// For this reason, it's recommended to only set to - /// if you are done writing to this instance. - /// - /// - /// Indentation is reset by this method, so the next write after calling flush will not - /// be indented. - /// - /// - /// The method is equivalent to calling this method with - /// set to . - /// - /// - public Task FlushAsync(bool insertNewLine) - { - var task = FlushCoreAsync(insertNewLine); - _asyncWriteTask = task; - return task; - } - - /// - /// Restarts writing on the beginning of the line, without indenting that line. - /// - /// - /// - /// The method will reset the output position to the beginning of the current line. - /// It does not modify the property, so the text will be indented again the next time - /// a line break is written to the output. - /// - /// - /// If the current line buffer is not empty, it will be flushed to the , followed by a new line - /// before the indentation is reset. If the current line buffer is empty (a line containing only indentation is considered empty), - /// the output position is simply reset to the beginning of the line without writing anything to the base writer. - /// - /// - public void ResetIndent() => ResetIndentCore(); - - /// - /// Restarts writing on the beginning of the line, without indenting that line. - /// - /// - /// A task that represents the asynchronous reset operation. - /// - /// - /// - /// The method will reset the output position to the beginning of the current line. - /// It does not modify the property, so the text will be indented again the next time - /// a line break is written to the output. - /// - /// - /// If the current line buffer is not empty, it will be flushed to the , followed by a new line - /// before the indentation is reset. If the current line buffer is empty (a line containing only indentation is considered empty), - /// the output position is simply reset to the beginning of the line without writing anything to the base writer. - /// - /// - public Task ResetIndentAsync() - { - var task = ResetIndentCoreAsync(); - _asyncWriteTask = task; - return task; - } - - /// - /// Returns a string representation of the current - /// instance. - /// - /// - /// If the property is an instance of the - /// class, the text written to this so far; otherwise, - /// the type name. - /// - /// - /// If the property is an instance of the - /// class, this method will return all text written to this - /// instance, including text that hasn't been flushed to the underlying - /// yet. It does this without flushing the buffer. - /// - /// - public override string? ToString() - { - if (_baseWriter is not StringWriter) - { - return base.ToString(); - } + /// + public override void Flush() => Flush(true); - if (_lineBuffer?.IsEmpty ?? true) - { - return _baseWriter.ToString(); - } + /// + public override Task FlushAsync() => FlushAsync(true); + + /// + /// Clears all buffers for this and causes any buffered data to be + /// written to the underlying writer, optionally inserting an additional new line. + /// + /// + /// Insert an additional new line if the line buffer is not empty. This has no effect if + /// the line buffer is empty or the property is zero. + /// + /// + /// + /// If is set to , the + /// class will not know the length of the flushed + /// line, and therefore the current line may not be correctly wrapped if more text is + /// written to the . + /// + /// + /// For this reason, it's recommended to only set to + /// if you are done writing to this instance. + /// + /// + /// Indentation is reset by this method, so the next write after calling flush will not + /// be indented. + /// + /// + /// The method is equivalent to calling this method with + /// set to . + /// + /// + public void Flush(bool insertNewLine) => FlushCore(insertNewLine); - using var tempWriter = new StringWriter(FormatProvider) { NewLine = NewLine }; - tempWriter.Write(_baseWriter.ToString()); - _lineBuffer.Peek(tempWriter); - return tempWriter.ToString(); + /// + /// Clears all buffers for this and causes any buffered data to be + /// written to the underlying writer, optionally inserting an additional new line. + /// + /// + /// Insert an additional new line if the line buffer is not empty. This has no effect if + /// the line buffer is empty or the property is zero. + /// + /// A token that can be used to cancel the operation. + /// A task that represents the asynchronous flush operation. + /// + /// + /// If is set to , the + /// class will not know the length of the flushed + /// line, and therefore the current line may not be correctly wrapped if more text is + /// written to the . + /// + /// + /// For this reason, it's recommended to only set to + /// if you are done writing to this instance. + /// + /// + /// Indentation is reset by this method, so the next write after calling flush will not + /// be indented. + /// + /// + /// The method is equivalent to calling this method with + /// set to . + /// + /// + public Task FlushAsync(bool insertNewLine, CancellationToken cancellationToken = default) + { + var task = FlushCoreAsync(insertNewLine, cancellationToken); + _asyncWriteTask = task; + return task; + } + + /// + /// Restarts writing on the beginning of the line, without indenting that line. + /// + /// + /// + /// The method will reset the output position to the beginning of the current line. + /// It does not modify the property, so the text will be indented again the next time + /// a line break is written to the output. + /// + /// + /// If the current line buffer is not empty, it will be flushed to the , followed by a new line + /// before the indentation is reset. If the current line buffer is empty (a line containing only indentation is considered empty), + /// the output position is simply reset to the beginning of the line without writing anything to the base writer. + /// + /// + public void ResetIndent() => ResetIndentCore(); + + /// + /// Restarts writing on the beginning of the line, without indenting that line. + /// + /// A token that can be used to cancel the operation. + /// + /// A task that represents the asynchronous reset operation. + /// + /// + /// + /// The method will reset the output position to the beginning of the current line. + /// It does not modify the property, so the text will be indented again the next time + /// a line break is written to the output. + /// + /// + /// If the current line buffer is not empty, it will be flushed to the , followed by a new line + /// before the indentation is reset. If the current line buffer is empty (a line containing only indentation is considered empty), + /// the output position is simply reset to the beginning of the line without writing anything to the base writer. + /// + /// + public Task ResetIndentAsync(CancellationToken cancellationToken = default) + { + var task = ResetIndentCoreAsync(cancellationToken); + _asyncWriteTask = task; + return task; + } + + /// + /// Returns a string representation of the current + /// instance. + /// + /// + /// If the property is an instance of the + /// class, the text written to this so far; otherwise, + /// the type name. + /// + /// + /// If the property is an instance of the + /// class, this method will return all text written to this + /// instance, including text that hasn't been flushed to the underlying + /// yet. It does this without flushing the buffer. + /// + /// + public override string? ToString() + { + if (_baseWriter is not StringWriter) + { + return base.ToString(); } - /// - protected override void Dispose(bool disposing) + if (_lineBuffer?.IsEmpty ?? true) { - Flush(); - base.Dispose(disposing); - if (disposing && _disposeBaseWriter) - { - _baseWriter.Dispose(); - } + return _baseWriter.ToString(); } - private partial void WriteNoMaximum(ReadOnlySpan buffer); + using var tempWriter = new StringWriter(FormatProvider) { NewLine = NewLine }; + tempWriter.Write(_baseWriter.ToString()); + _lineBuffer.Peek(tempWriter); + return tempWriter.ToString(); + } - private partial void WriteLineBreakDirect(); + /// + protected override void Dispose(bool disposing) + { + Flush(); + base.Dispose(disposing); + if (disposing && _disposeBaseWriter) + { + _baseWriter.Dispose(); + } + } - private partial void WriteIndentDirectIfNeeded(); + private partial void WriteNoMaximum(ReadOnlySpan buffer); - private static partial void WriteIndent(TextWriter writer, int indent); + private partial void WriteLineBreakDirect(); - private partial void WriteCore(ReadOnlySpan buffer); + private partial void WriteIndentDirectIfNeeded(); - private partial void FlushCore(bool insertNewLine); + private static partial void WriteIndent(TextWriter writer, int indent); - private partial void ResetIndentCore(); + private partial void WriteCore(ReadOnlySpan buffer); - private static int GetLineLengthForConsole() + private partial void FlushCore(bool insertNewLine); + + private partial void ResetIndentCore(); + + private static partial void WriteBlankLine(TextWriter writer); + + private static int GetLineLengthForConsole() + { + try { - try - { - return Console.WindowWidth - 1; - } - catch (IOException) - { - return 0; - } + return Console.WindowWidth - 1; + } + catch (IOException) + { + return 0; } + } - private void ThrowIfWriteInProgress() + private void ThrowIfWriteInProgress() + { + if (!_asyncWriteTask.IsCompleted) { - if (!_asyncWriteTask.IsCompleted) - { - throw new InvalidOperationException(Properties.Resources.AsyncWriteInProgress); - } + throw new InvalidOperationException(Properties.Resources.AsyncWriteInProgress); } } } diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs index 644cb88e..471dc8c1 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs @@ -1,162 +1,162 @@ -using Ookii.CommandLine.Properties; +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Properties; using System; using System.Diagnostics; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +public partial class LocalizedStringProvider { - public partial class LocalizedStringProvider + /// + /// Gets the error message for . + /// + /// The error message. + /// + /// + /// Ookii.CommandLine never creates exceptions with this category, so this should not + /// normally be called. + /// + /// + public virtual string UnspecifiedError() => Resources.UnspecifiedError; + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The value of the argument. + /// The value description of the argument. + /// The error message. + public virtual string ArgumentValueConversionError(string argumentName, string? argumentValue, string valueDescription) + => Format(Resources.ArgumentConversionErrorFormat, argumentValue, argumentName, valueDescription); + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The error message. + public virtual string UnknownArgument(string argumentName) => Format(Resources.UnknownArgumentFormat, argumentName); + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The error message. + public virtual string MissingNamedArgumentValue(string argumentName) + => Format(Resources.MissingValueForNamedArgumentFormat, argumentName); + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The error message. + public virtual string DuplicateArgument(string argumentName) => Format(Resources.DuplicateArgumentFormat, argumentName); + + /// + /// Gets the warning message used if the + /// or property is . + /// + /// The name of the argument. + /// The warning message. + public virtual string DuplicateArgumentWarning(string argumentName) => Format(Resources.DuplicateArgumentWarningFormat, argumentName); + + /// + /// Gets the error message for . + /// + /// The error message. + public virtual string TooManyArguments() => Resources.TooManyArguments; + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The error message. + public virtual string MissingRequiredArgument(string argumentName) + => Format(Resources.MissingRequiredArgumentFormat, argumentName); + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The value of the argument. + /// The error message of the exception that caused this error. + /// The error message. + public virtual string InvalidDictionaryValue(string argumentName, string? argumentValue, string? message) + => Format(Resources.InvalidDictionaryValueFormat, argumentName, argumentValue, message); + + /// + /// Gets the error message for . + /// + /// The error message from instantiating the type. + /// The error message. + public virtual string CreateArgumentsTypeError(string? message) + => Format(Resources.CreateArgumentsTypeErrorFormat, message); + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The error message from setting the value. + /// The error message. + public virtual string ApplyValueError(string argumentName, string? message) + => Format(Resources.SetValueErrorFormat, argumentName, message); + + /// + /// Gets the error message for . + /// + /// The name of the argument. + /// The error message. + public virtual string NullArgumentValue(string argumentName) => Format(Resources.NullArgumentValueFormat, argumentName); + + /// + /// Gets the error message for . + /// + /// The names of the combined short arguments. + /// The error message. + public virtual string CombinedShortNameNonSwitch(string argumentName) + => Format(Resources.CombinedShortNameNonSwitchFormat, argumentName); + + /// + /// Gets the error message used if the + /// is unable to find the key/value pair separator in the argument value. + /// + /// The key/value pair separator. + /// The error message. + public virtual string MissingKeyValuePairSeparator(string separator) + => Format(Resources.NoKeyValuePairSeparatorFormat, separator); + + internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, Exception? inner, CommandLineArgument argument, string? value = null) + => CreateException(category, inner, argument, argument.ArgumentName, value); + + internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, Exception? inner, string? argumentName = null, string? value = null) + => CreateException(category, inner, null, argumentName, value); + + internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, CommandLineArgument argument, string? value = null) + => CreateException(category, null, argument, value); + + internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, string? argumentName = null, string? value = null) + => CreateException(category, null, argumentName, value); + + private CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, Exception? inner, CommandLineArgument? argument = null, string? argumentName = null, string? value = null) { - /// - /// Gets the error message for . - /// - /// The error message. - /// - /// - /// Ookii.CommandLine never creates exceptions with this category, so this should not - /// normally be called. - /// - /// - public virtual string UnspecifiedError() => Resources.UnspecifiedError; - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The value of the argument. - /// The value description of the argument. - /// The error message. - public virtual string ArgumentValueConversionError(string argumentName, string? argumentValue, string valueDescription) - => Format(Resources.ArgumentConversionErrorFormat, argumentValue, argumentName, valueDescription); - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The error message. - public virtual string UnknownArgument(string argumentName) => Format(Resources.UnknownArgumentFormat, argumentName); - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The error message. - public virtual string MissingNamedArgumentValue(string argumentName) - => Format(Resources.MissingValueForNamedArgumentFormat, argumentName); - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The error message. - public virtual string DuplicateArgument(string argumentName) => Format(Resources.DuplicateArgumentFormat, argumentName); - - /// - /// Gets the warning message used if the - /// or property is . - /// - /// The name of the argument. - /// The error message. - public virtual string DuplicateArgumentWarning(string argumentName) => Format(Resources.DuplicateArgumentWarningFormat, argumentName); - - /// - /// Gets the error message for . - /// - /// The error message. - public virtual string TooManyArguments() => Resources.TooManyArguments; - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The error message. - public virtual string MissingRequiredArgument(string argumentName) - => Format(Resources.MissingRequiredArgumentFormat, argumentName); - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The value of the argument. - /// The error message of the conversion. - /// The error message. - public virtual string InvalidDictionaryValue(string argumentName, string? argumentValue, string? message) - => Format(Resources.InvalidDictionaryValueFormat, argumentName, argumentValue, message); - - /// - /// Gets the error message for . - /// - /// The error message of the conversion. - /// The error message. - public virtual string CreateArgumentsTypeError(string? message) - => Format(Resources.CreateArgumentsTypeErrorFormat, message); - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The error message of the conversion. - /// The error message. - public virtual string ApplyValueError(string argumentName, string? message) - => Format(Resources.SetValueErrorFormat, argumentName, message); - - /// - /// Gets the error message for . - /// - /// The name of the argument. - /// The error message. - public virtual string NullArgumentValue(string argumentName) => Format(Resources.NullArgumentValueFormat, argumentName); - - /// - /// Gets the error message for . - /// - /// The names of the combined short arguments. - /// The error message. - public virtual string CombinedShortNameNonSwitch(string argumentName) - => Format(Resources.CombinedShortNameNonSwitchFormat, argumentName); - - /// - /// Gets the error message used if the - /// is unable to find the key/value pair separator in the argument value. - /// - /// The key/value pair separator. - /// The error message. - public virtual string MissingKeyValuePairSeparator(string separator) - => Format(Resources.NoKeyValuePairSeparatorFormat, separator); - - internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, Exception? inner, CommandLineArgument argument, string? value = null) - => CreateException(category, inner, argument, argument.ArgumentName, value); - - internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, Exception? inner, string? argumentName = null, string? value = null) - => CreateException(category, inner, null, argumentName, value); - - internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, CommandLineArgument argument, string? value = null) - => CreateException(category, null, argument, value); - - internal CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, string? argumentName = null, string? value = null) - => CreateException(category, null, argumentName, value); - - private CommandLineArgumentException CreateException(CommandLineArgumentErrorCategory category, Exception? inner, CommandLineArgument? argument = null, string? argumentName = null, string? value = null) + // These are not created using the helper, because there is not one standard message. + Debug.Assert(category != CommandLineArgumentErrorCategory.ValidationFailed); + + var message = category switch { - // These are not created using the helper, because there is not one standard message. - Debug.Assert(category != CommandLineArgumentErrorCategory.ValidationFailed); - - var message = category switch - { - CommandLineArgumentErrorCategory.MissingRequiredArgument => MissingRequiredArgument(argumentName!), - CommandLineArgumentErrorCategory.ArgumentValueConversion => ArgumentValueConversionError(argumentName!, value, argument!.ValueDescription), - CommandLineArgumentErrorCategory.UnknownArgument => UnknownArgument(argumentName!), - CommandLineArgumentErrorCategory.MissingNamedArgumentValue => MissingNamedArgumentValue(argumentName!), - CommandLineArgumentErrorCategory.DuplicateArgument => DuplicateArgument(argumentName!), - CommandLineArgumentErrorCategory.TooManyArguments => TooManyArguments(), - CommandLineArgumentErrorCategory.InvalidDictionaryValue => InvalidDictionaryValue(argumentName!, value, inner?.Message), - CommandLineArgumentErrorCategory.CreateArgumentsTypeError => CreateArgumentsTypeError(inner?.Message), - CommandLineArgumentErrorCategory.ApplyValueError => ApplyValueError(argumentName!, inner?.Message), - CommandLineArgumentErrorCategory.NullArgumentValue => NullArgumentValue(argumentName!), - CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch => CombinedShortNameNonSwitch(argumentName!), - _ => UnspecifiedError(), - }; - - return new CommandLineArgumentException(message, argumentName, category, inner); - } + CommandLineArgumentErrorCategory.MissingRequiredArgument => MissingRequiredArgument(argumentName!), + CommandLineArgumentErrorCategory.ArgumentValueConversion => ArgumentValueConversionError(argumentName!, value, argument!.ValueDescription), + CommandLineArgumentErrorCategory.UnknownArgument => UnknownArgument(argumentName!), + CommandLineArgumentErrorCategory.MissingNamedArgumentValue => MissingNamedArgumentValue(argumentName!), + CommandLineArgumentErrorCategory.DuplicateArgument => DuplicateArgument(argumentName!), + CommandLineArgumentErrorCategory.TooManyArguments => TooManyArguments(), + CommandLineArgumentErrorCategory.InvalidDictionaryValue => InvalidDictionaryValue(argumentName!, value, inner?.Message), + CommandLineArgumentErrorCategory.CreateArgumentsTypeError => CreateArgumentsTypeError(inner?.Message), + CommandLineArgumentErrorCategory.ApplyValueError => ApplyValueError(argumentName!, inner?.Message), + CommandLineArgumentErrorCategory.NullArgumentValue => NullArgumentValue(argumentName!), + CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch => CombinedShortNameNonSwitch(argumentName!), + _ => UnspecifiedError(), + }; + + return new CommandLineArgumentException(message, argumentName, category, inner); } } diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs index 5db1dac7..51285244 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs @@ -4,307 +4,314 @@ using System.Collections.Generic; using System.Linq; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +public partial class LocalizedStringProvider { - public partial class LocalizedStringProvider + private const string ArgumentSeparator = ", "; + + /// + /// Gets a formatted list of validator help messages. + /// + /// The command line argument. + /// The string. + /// + /// + /// The default implementation of expects the returned + /// value to start with a white-space character. + /// + /// + /// If you override the + /// method, this method will not be called. + /// + /// + public virtual string ValidatorDescriptions(CommandLineArgument argument) { - private const string ArgumentSeparator = ", "; + var messages = argument.Validators + .Select(v => v.GetUsageHelp(argument)) + .Where(h => !string.IsNullOrEmpty(h)); - /// - /// Gets a formatted list of validator help messages. - /// - /// The command line argument. - /// The string. - /// - /// - /// The default implementation of expects the returned - /// value to start with a white-space character. - /// - /// - /// If you override the method, this method will not be called. - /// - /// - public virtual string ValidatorDescriptions(CommandLineArgument argument) + var result = string.Join(" ", messages); + if (result.Length > 0) { - var messages = argument.Validators - .Select(v => v.GetUsageHelp(argument)) - .Where(h => !string.IsNullOrEmpty(h)); + result = " " + result; + } - var result = string.Join(" ", messages); - if (result.Length > 0) - { - result = " " + result; - } + return result; + } - return result; + /// + /// Gets the usage help for the attribute. + /// + /// The attribute instance. + /// The string. + public virtual string ValidateCountUsageHelp(ValidateCountAttribute attribute) + { + if (attribute.Minimum <= 0) + { + return Format(Resources.ValidateCountUsageHelpMaxFormat, attribute.Maximum); } - - /// - /// Gets the usage help for the class. - /// - /// The attribute instance. - /// The string. - public virtual string ValidateCountUsageHelp(ValidateCountAttribute attribute) + else if (attribute.Maximum == int.MaxValue) { - if (attribute.Minimum <= 0) - { - return Format(Resources.ValidateCountUsageHelpMaxFormat, attribute.Maximum); - } - else if (attribute.Maximum == int.MaxValue) - { - return Format(Resources.ValidateCountUsageHelpMinFormat, attribute.Minimum); - } - - return Format(Resources.ValidateCountUsageHelpBothFormat, attribute.Minimum, attribute.Maximum); + return Format(Resources.ValidateCountUsageHelpMinFormat, attribute.Minimum); } - /// - /// Gets the usage help for the class. - /// - /// The string. - public virtual string ValidateNotEmptyUsageHelp() - => Resources.ValidateNotEmptyUsageHelp; + return Format(Resources.ValidateCountUsageHelpBothFormat, attribute.Minimum, attribute.Maximum); + } + + /// + /// Gets the usage help for the attribute. + /// + /// The string. + public virtual string ValidateNotEmptyUsageHelp() + => Resources.ValidateNotEmptyUsageHelp; - /// - /// Gets the usage help for the class. - /// - /// The string. - public virtual string ValidateNotWhiteSpaceUsageHelp() - => Resources.ValidateNotWhiteSpaceUsageHelp; + /// + /// Gets the usage help for the attribute. + /// + /// The string. + public virtual string ValidateNotWhiteSpaceUsageHelp() + => Resources.ValidateNotWhiteSpaceUsageHelp; - /// - /// Gets the usage help for the class. - /// - /// The attribute instance. - /// The string. - public virtual string ValidateRangeUsageHelp(ValidateRangeAttribute attribute) + /// + /// Gets the usage help for the attribute. + /// + /// The attribute instance. + /// The string. + public virtual string ValidateRangeUsageHelp(ValidateRangeAttribute attribute) + { + if (attribute.Minimum == null) { - if (attribute.Minimum == null) - { - return Format(Resources.ValidateRangeUsageHelpMaxFormat, attribute.Maximum); - } - else if (attribute.Maximum == null) - { - return Format(Resources.ValidateRangeUsageHelpMinFormat, attribute.Minimum); - } - - return Format(Resources.ValidateRangeUsageHelpBothFormat, attribute.Minimum, attribute.Maximum); + return Format(Resources.ValidateRangeUsageHelpMaxFormat, attribute.Maximum); } - - /// - /// Gets the usage help for the class. - /// - /// The attribute instance. - /// The string. - public virtual string ValidateStringLengthUsageHelp(ValidateStringLengthAttribute attribute) + else if (attribute.Maximum == null) { - if (attribute.Minimum <= 0) - { - return Format(Resources.ValidateStringLengthUsageHelpMaxFormat, attribute.Maximum); - } - else if (attribute.Maximum == int.MaxValue) - { - return Format(Resources.ValidateStringLengthUsageHelpMinFormat, attribute.Minimum); - } + return Format(Resources.ValidateRangeUsageHelpMinFormat, attribute.Minimum); + } + + return Format(Resources.ValidateRangeUsageHelpBothFormat, attribute.Minimum, attribute.Maximum); + } - return Format(Resources.ValidateStringLengthUsageHelpBothFormat, attribute.Minimum, attribute.Maximum); + /// + /// Gets the usage help for the attribute. + /// + /// The attribute instance. + /// The string. + public virtual string ValidateStringLengthUsageHelp(ValidateStringLengthAttribute attribute) + { + if (attribute.Minimum <= 0) + { + return Format(Resources.ValidateStringLengthUsageHelpMaxFormat, attribute.Maximum); + } + else if (attribute.Maximum == int.MaxValue) + { + return Format(Resources.ValidateStringLengthUsageHelpMinFormat, attribute.Minimum); } - /// - /// Gets the usage help for the class. - /// - /// The enumeration type. - /// The string. - public virtual string ValidateEnumValueUsageHelp(Type enumType) - => Format(Resources.ValidateEnumValueUsageHelpFormat, string.Join(ArgumentSeparator, Enum.GetNames(enumType))); + return Format(Resources.ValidateStringLengthUsageHelpBothFormat, attribute.Minimum, attribute.Maximum); + } + /// + /// Gets the usage help for the attribute. + /// + /// The enumeration type. + /// The string. + public virtual string ValidateEnumValueUsageHelp(Type enumType) + => Format(Resources.ValidateEnumValueUsageHelpFormat, string.Join(ArgumentSeparator, Enum.GetNames(enumType))); - /// - /// Gets the usage help for the class. - /// - /// The prohibited arguments. - /// The string. - public virtual string ProhibitsUsageHelp(IEnumerable arguments) - => Format(Resources.ValidateProhibitsUsageHelpFormat, - string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); - /// - /// Gets the usage help for the class. - /// - /// The required arguments. - /// The string. - public virtual string RequiresUsageHelp(IEnumerable arguments) - => Format(Resources.ValidateRequiresUsageHelpFormat, - string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); + /// + /// Gets the usage help for the attribute. + /// + /// The prohibited arguments. + /// The string. + public virtual string ProhibitsUsageHelp(IEnumerable arguments) + => Format(Resources.ValidateProhibitsUsageHelpFormat, + string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); - /// - /// Gets an error message used if the fails validation. - /// - /// The names of the arguments. - /// The error message. - public virtual string RequiresAnyUsageHelp(IEnumerable arguments) - { - // This deliberately reuses the error messge. - return Format(Resources.ValidateRequiresAnyFailedFormat, - string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); - } + /// + /// Gets the usage help for the attribute. + /// + /// The required arguments. + /// The string. + public virtual string RequiresUsageHelp(IEnumerable arguments) + => Format(Resources.ValidateRequiresUsageHelpFormat, + string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); - /// - /// Gets a generic error message for the base implementation of . - /// - /// The name of the argument. - /// The error message. - public virtual string ValidationFailed(string argumentName) - => Format(Resources.ValidationFailedFormat, argumentName); + /// + /// Gets the usage help for the attribute. + /// + /// The names of the arguments. + /// The string. + public virtual string RequiresAnyUsageHelp(IEnumerable arguments) + { + // This deliberately reuses the error messge. + return Format(Resources.ValidateRequiresAnyFailedFormat, + string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); + } + + /// + /// Gets a generic error message for the base implementation of . + /// + /// The name of the argument. + /// The error message. + public virtual string ValidationFailed(string argumentName) + => Format(Resources.ValidationFailedFormat, argumentName); - /// - /// Gets a generic error message for the base implementation of . - /// - /// The error message. - public virtual string ClassValidationFailed() => Resources.ClassValidationFailed; + /// + /// Gets a generic error message for the base implementation of . + /// + /// The error message. + public virtual string ClassValidationFailed() => Resources.ClassValidationFailed; - /// - /// Gets an error message used if the fails validation. - /// - /// The name of the argument. - /// The . - /// The error message. - public virtual string ValidateRangeFailed(string argumentName, ValidateRangeAttribute attribute) + /// + /// Gets an error message used if the attribute fails + /// validation. + /// + /// The name of the argument. + /// The . + /// The error message. + public virtual string ValidateRangeFailed(string argumentName, ValidateRangeAttribute attribute) + { + if (attribute.Maximum == null) + { + return Format(Resources.ValidateRangeFailedMinFormat, argumentName, attribute.Minimum); + } + else if (attribute.Minimum == null) + { + return Format(Resources.ValidateRangeFailedMaxFormat, argumentName, attribute.Maximum); + } + else { - if (attribute.Maximum == null) - { - return Format(Resources.ValidateRangeFailedMinFormat, argumentName, attribute.Minimum); - } - else if (attribute.Minimum == null) - { - return Format(Resources.ValidateRangeFailedMaxFormat, argumentName, attribute.Maximum); - } - else - { - return Format(Resources.ValidateRangeFailedBothFormat, argumentName, attribute.Minimum, attribute.Maximum); - } + return Format(Resources.ValidateRangeFailedBothFormat, argumentName, attribute.Minimum, attribute.Maximum); } + } - /// - /// Gets an error message used if the fails - /// validation because the string was empty. - /// - /// The name of the argument. - /// The error message. - /// - /// - /// If failed because the value was - /// , the method is called instead. - /// - /// - public virtual string ValidateNotEmptyFailed(string argumentName) - => Format(Resources.ValidateNotEmptyFailedFormat, argumentName); + /// + /// Gets an error message used if the attribute fails + /// validation because the string was empty. + /// + /// The name of the argument. + /// The error message. + /// + /// + /// If failed because the value was + /// , the method is called instead. + /// + /// + public virtual string ValidateNotEmptyFailed(string argumentName) + => Format(Resources.ValidateNotEmptyFailedFormat, argumentName); - /// - /// Gets an error message used if the fails - /// validation because the string was empty. - /// - /// The name of the argument. - /// The error message. - /// - /// - /// If failed because the value was - /// , the method is called instead. - /// - /// - public virtual string ValidateNotWhiteSpaceFailed(string argumentName) - => Format(Resources.ValidateNotWhiteSpaceFailedFormat, argumentName); + /// + /// Gets an error message used if the fails + /// validation because the string was empty or white-space. + /// + /// The name of the argument. + /// The error message. + /// + /// + /// If failed because the value was + /// , the method is called instead. + /// + /// + public virtual string ValidateNotWhiteSpaceFailed(string argumentName) + => Format(Resources.ValidateNotWhiteSpaceFailedFormat, argumentName); - /// - /// Gets an error message used if the fails validation. - /// - /// The name of the argument. - /// The . - /// The error message. - public virtual string ValidateStringLengthFailed(string argumentName, ValidateStringLengthAttribute attribute) + /// + /// Gets an error message used if the attribute + /// fails validation. + /// + /// The name of the argument. + /// The . + /// The error message. + public virtual string ValidateStringLengthFailed(string argumentName, ValidateStringLengthAttribute attribute) + { + if (attribute.Maximum == int.MaxValue) { - if (attribute.Maximum == int.MaxValue) - { - return Format(Resources.ValidateStringLengthMinFormat, argumentName, attribute.Minimum); - } - else if (attribute.Minimum <= 0) - { - return Format(Resources.ValidateStringLengthMaxFormat, argumentName, attribute.Maximum); - } - else - { - return Format(Resources.ValidateStringLengthBothFormat, argumentName, attribute.Minimum, attribute.Maximum); - } + return Format(Resources.ValidateStringLengthMinFormat, argumentName, attribute.Minimum); } - - /// - /// Gets an error message used if the fails validation. - /// - /// The name of the argument. - /// The . - /// The error message. - public virtual string ValidateCountFailed(string argumentName, ValidateCountAttribute attribute) + else if (attribute.Minimum <= 0) + { + return Format(Resources.ValidateStringLengthMaxFormat, argumentName, attribute.Maximum); + } + else { - if (attribute.Maximum == int.MaxValue) - { - return Format(Resources.ValidateCountMinFormat, argumentName, attribute.Minimum); - } - else if (attribute.Minimum <= 0) - { - return Format(Resources.ValidateCountMaxFormat, argumentName, attribute.Maximum); - } - else - { - return Format(Resources.ValidateCountBothFormat, argumentName, attribute.Minimum, attribute.Maximum); - } + return Format(Resources.ValidateStringLengthBothFormat, argumentName, attribute.Minimum, attribute.Maximum); } + } - /// - /// Gets an error message used if the fails validation. - /// - /// The name of the argument. - /// The type of the enumeration. - /// The value of the argument. - /// - /// to include the possible values of the enumeration in the error - /// message; otherwise, . - /// - /// The error message. - public virtual string ValidateEnumValueFailed(string argumentName, Type enumType, object? value, bool includeValues) + /// + /// Gets an error message used if the attribute fails + /// validation. + /// + /// The name of the argument. + /// The . + /// The error message. + public virtual string ValidateCountFailed(string argumentName, ValidateCountAttribute attribute) + { + if (attribute.Maximum == int.MaxValue) + { + return Format(Resources.ValidateCountMinFormat, argumentName, attribute.Minimum); + } + else if (attribute.Minimum <= 0) + { + return Format(Resources.ValidateCountMaxFormat, argumentName, attribute.Maximum); + } + else { - return includeValues - ? Format(Resources.ValidateEnumValueFailedWithValuesFormat, argumentName, string.Join(ArgumentSeparator, - Enum.GetNames(enumType))) - : Format(Resources.ValidateEnumValueFailedFormat, value, argumentName); + return Format(Resources.ValidateCountBothFormat, argumentName, attribute.Minimum, attribute.Maximum); } + } - /// - /// Gets an error message used if the fails validation. - /// - /// The name of the argument. - /// The names of the required arguments. - /// The error message. - public virtual string ValidateRequiresFailed(string argumentName, IEnumerable dependencies) - => Format(Resources.ValidateRequiresFailedFormat, argumentName, - string.Join(ArgumentSeparator, dependencies.Select(a => a.ArgumentNameWithPrefix))); + /// + /// Gets an error message used if the attribute fails + /// validation. + /// + /// The name of the argument. + /// The type of the enumeration. + /// The value of the argument. + /// + /// to include the possible values of the enumeration in the error + /// message; otherwise, . + /// + /// The error message. + public virtual string ValidateEnumValueFailed(string argumentName, Type enumType, object? value, bool includeValues) + { + return includeValues + ? Format(Resources.ValidateEnumValueFailedWithValuesFormat, argumentName, string.Join(ArgumentSeparator, + Enum.GetNames(enumType))) + : Format(Resources.ValidateEnumValueFailedFormat, value, argumentName); + } - /// - /// Gets an error message used if the fails validation. - /// - /// The name of the argument. - /// The names of the prohibited arguments. - /// The error message. - public virtual string ValidateProhibitsFailed(string argumentName, IEnumerable prohibitedArguments) - => Format(Resources.ValidateProhibitsFailedFormat, argumentName, - string.Join(ArgumentSeparator, prohibitedArguments.Select(a => a.ArgumentNameWithPrefix))); + /// + /// Gets an error message used if the attribute fails + /// validation. + /// + /// The name of the argument. + /// The names of the required arguments. + /// The error message. + public virtual string ValidateRequiresFailed(string argumentName, IEnumerable dependencies) + => Format(Resources.ValidateRequiresFailedFormat, argumentName, + string.Join(ArgumentSeparator, dependencies.Select(a => a.ArgumentNameWithPrefix))); - /// - /// Gets an error message used if the fails validation. - /// - /// The names of the arguments. - /// The error message. - public virtual string ValidateRequiresAnyFailed(IEnumerable arguments) - => Format(Resources.ValidateRequiresAnyFailedFormat, - string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); - } + /// + /// Gets an error message used if the attribute fails + /// validation. + /// + /// The name of the argument. + /// The names of the prohibited arguments. + /// The error message. + public virtual string ValidateProhibitsFailed(string argumentName, IEnumerable prohibitedArguments) + => Format(Resources.ValidateProhibitsFailedFormat, argumentName, + string.Join(ArgumentSeparator, prohibitedArguments.Select(a => a.ArgumentNameWithPrefix))); + + /// + /// Gets an error message used if the attribute fails + /// validation. + /// + /// The names of the arguments. + /// The error message. + public virtual string ValidateRequiresAnyFailed(IEnumerable arguments) + => Format(Resources.ValidateRequiresAnyFailedFormat, + string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); } diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.cs b/src/Ookii.CommandLine/LocalizedStringProvider.cs index af83c8be..b2ccec0f 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.cs @@ -3,130 +3,132 @@ using System.Globalization; using System.Reflection; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides custom localized strings. +/// +/// +/// +/// Inherit from this class and override its members to provide customized or localized +/// strings. You can specify the implementation to use with the +/// property. +/// +/// +/// For error messages, this only lets you customize error messages for the +/// class. Other exceptions thrown by this library, +/// such as for invalid argument definitions, constitute bugs and should not occur in a +/// correct program, and should therefore not be shown to the user. +/// +/// +/// +public partial class LocalizedStringProvider { /// - /// Provides custom localized strings. + /// Gets the name of the help argument created if the + /// or property is . /// + /// The string. + public virtual string AutomaticHelpName() => Resources.AutomaticHelpName; + + /// + /// Gets the short name of the help argument created if the + /// property is , typically '?'. + /// + /// The string. /// /// - /// Inherit from this class and override its members to provide customized or localized - /// strings. You can specify the implementation to use using . + /// The argument will automatically have a short alias that is the lower case first + /// character of the value returned by . If the character + /// returned by this method is the same as that character according to the + /// property, then no alias is added. + /// + /// + /// If is not , + /// the short name and the short alias will be used as a regular aliases instead. /// - /// - /// For error messages, this only lets you customize error messages for the - /// class. Other exceptions thrown by this library, - /// such as for invalid argument definitions, constitute bugs and should not occur in a - /// correct program, and should therefore not be shown to the user. - /// /// - public partial class LocalizedStringProvider - { - /// - /// Gets the name of the help argument created if the - /// or property is . - /// - /// The string. - public virtual string AutomaticHelpName() => Resources.AutomaticHelpName; + public virtual char AutomaticHelpShortName() => Resources.AutomaticHelpShortName[0]; - /// - /// Gets the short name of the help argument created if the - /// property is , typically '?'. - /// - /// The string. - /// - /// - /// The argument will automatically have a short alias that is the lower case first - /// character of the value returned by . If this character - /// is the same according to the argument name comparer, then no alias is added. - /// - /// - /// If is not , - /// the short name and the short alias will be used as a regular aliases instead. - /// - /// - public virtual char AutomaticHelpShortName() => Resources.AutomaticHelpShortName[0]; - - /// - /// Gets the description of the help argument created if the - /// property is . - /// - /// The string. - public virtual string AutomaticHelpDescription() => Resources.AutomaticHelpDescription; + /// + /// Gets the description of the help argument created if the + /// property is . + /// + /// The string. + public virtual string AutomaticHelpDescription() => Resources.AutomaticHelpDescription; - /// - /// Gets the name of the version argument created if the - /// property is . - /// - /// The string. - public virtual string AutomaticVersionName() => Resources.AutomaticVersionName; + /// + /// Gets the name of the version argument created if the + /// property is . + /// + /// The string. + public virtual string AutomaticVersionName() => Resources.AutomaticVersionName; - /// - /// Gets the description of the version argument created if the - /// property is . - /// - /// The string. - public virtual string AutomaticVersionDescription() => Resources.AutomaticVersionDescription; + /// + /// Gets the description of the version argument created if the + /// property is . + /// + /// The string. + public virtual string AutomaticVersionDescription() => Resources.AutomaticVersionDescription; - /// - /// Gets the name of the version command created if the - /// property is . - /// - /// The string. - public virtual string AutomaticVersionCommandName() => Resources.AutomaticVersionCommandName; + /// + /// Gets the name of the version command created if the + /// property is . + /// + /// The string. + public virtual string AutomaticVersionCommandName() => Resources.AutomaticVersionCommandName; - /// - /// Gets the description of the version command created if the - /// property is . - /// - /// The string. - public virtual string AutomaticVersionCommandDescription() => Resources.AutomaticVersionDescription; + /// + /// Gets the description of the version command created if the + /// property is . + /// + /// The string. + public virtual string AutomaticVersionCommandDescription() => Resources.AutomaticVersionDescription; - /// - /// Gets the name and version of the application, used by the automatic version argument - /// and command. - /// - /// The assembly whose version to use. - /// - /// The friendly name of the application; typically the value of the - /// property. - /// - /// The string. - /// - /// - /// The base implementation uses the , - /// and will fall back to the assembly version if none is defined. - /// - /// - public virtual string ApplicationNameAndVersion(Assembly assembly, string friendlyName) - { - var versionAttribute = assembly.GetCustomAttribute(); - var version = versionAttribute?.InformationalVersion ?? assembly.GetName().Version?.ToString() ?? string.Empty; - return $"{friendlyName} {version}"; - } + /// + /// Gets the name and version of the application, used by the automatic version argument + /// and command. + /// + /// The assembly whose version to use. + /// + /// The friendly name of the application; typically the value of the + /// property. + /// + /// The string. + /// + /// + /// The base implementation uses the + /// attribute, and will fall back to the assembly version if none is defined. + /// + /// + public virtual string ApplicationNameAndVersion(Assembly assembly, string friendlyName) + { + var versionAttribute = assembly.GetCustomAttribute(); + var version = versionAttribute?.InformationalVersion ?? assembly.GetName().Version?.ToString() ?? string.Empty; + return $"{friendlyName} {version}"; + } - /// - /// Gets the copyright information for the application, used by the automatic version - /// argument and command. - /// - /// The assembly whose copyright information to use. - /// The string. - /// - /// - /// The base implementation returns the value of the , - /// or if none is defined. - /// - /// - public virtual string? ApplicationCopyright(Assembly assembly) - => assembly.GetCustomAttribute()?.Copyright; + /// + /// Gets the copyright information for the application, used by the automatic version + /// argument and command. + /// + /// The assembly whose copyright information to use. + /// The string. + /// + /// + /// The base implementation returns the value of the , + /// or if none is defined. + /// + /// + public virtual string? ApplicationCopyright(Assembly assembly) + => assembly.GetCustomAttribute()?.Copyright; - private static string Format(string format, object? arg0) - => string.Format(CultureInfo.CurrentCulture, format, arg0); + private static string Format(string format, object? arg0) + => string.Format(CultureInfo.CurrentCulture, format, arg0); - private static string Format(string format, object? arg0, object? arg1) - => string.Format(CultureInfo.CurrentCulture, format, arg0, arg1); + private static string Format(string format, object? arg0, object? arg1) + => string.Format(CultureInfo.CurrentCulture, format, arg0, arg1); - private static string Format(string format, object? arg0, object? arg1, object? arg2) - => string.Format(CultureInfo.CurrentCulture, format, arg0, arg1, arg2); - } + private static string Format(string format, object? arg0, object? arg1, object? arg2) + => string.Format(CultureInfo.CurrentCulture, format, arg0, arg1, arg2); } diff --git a/src/Ookii.CommandLine/MultiValueArgumentInfo.cs b/src/Ookii.CommandLine/MultiValueArgumentInfo.cs new file mode 100644 index 00000000..c42281b3 --- /dev/null +++ b/src/Ookii.CommandLine/MultiValueArgumentInfo.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine; + +/// +/// Provides information that only applies to multi-value and dictionary arguments. +/// +/// +public sealed class MultiValueArgumentInfo +{ + /// + /// Creates a new instance of the class. + /// + /// + /// The separator between multiple values in the same token, or if no + /// separator is used. + /// + /// + /// if the argument can consume multiple tokens; otherwise, + /// . + /// + public MultiValueArgumentInfo(string? separator, bool allowWhiteSpaceSeparator) + { + Separator = separator; + AllowWhiteSpaceSeparator = allowWhiteSpaceSeparator; + } + + + /// + /// Gets the separator that can be used to supply multiple values in a single argument token. + /// + /// + /// The separator, or if no separator is used. + /// + /// + public string? Separator { get; } + + /// + /// Gets a value that indicates whether or not the argument can consume multiple following + /// argument tokens. + /// + /// + /// if the argument consume multiple following tokens; otherwise, + /// . + /// + /// + /// + /// A multi-value argument that allows white-space separators is able to consume multiple + /// values from the command line that follow it. All values that follow the name, up until + /// the next argument name, are considered values for this argument. + /// + /// + /// + public bool AllowWhiteSpaceSeparator { get; internal set; } +} diff --git a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs index f0621edb..e3a91082 100644 --- a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs +++ b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs @@ -1,109 +1,108 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Specifies a separator for the values of multi-value arguments. +/// +/// +/// +/// Normally, you need to supply the argument multiple times to set multiple values, e.g. +/// by using -Sample Value1 -Sample Value2. If you specify the +/// attribute, it allows you to specify multiple values with a single argument by using a +/// separator. +/// +/// +/// There are two ways you can use separators for multi-value arguments: a white-space +/// separator, or an explicit separator string. +/// +/// +/// You enable the use of white-space separators with the +/// constructor. A multi-value argument that allows white-space separators is able to consume +/// multiple values from the command line that follow it. All values that follow the name, up +/// until the next argument name, are considered values for this argument. +/// +/// +/// For example, if you use -Sample Value1 Value2 Value3, all three arguments after +/// -Sample are taken as values. In this case, it's not possible to supply any +/// positional arguments until another named argument has been supplied. +/// +/// +/// Using white-space separators will not work if the +/// property is or if the argument is a multi-value switch argument. +/// +/// +/// Using the constructor, you instead +/// specify an explicit character sequence to be used as a separator. For example, if the +/// separator is set to a comma, you can use -Sample Value1,Value2. +/// +/// +/// If you specify an explicit separator for a multi-value argument, it will not be +/// possible to use the separator in the individual argument values. There is no way to +/// escape it. +/// +/// +/// Even if the is specified it is still possible to use +/// multiple arguments to specify multiple values. For example, using a comma as the separator, +/// -Sample Value1,Value2 -Sample Value3 will mean the argument "Sample" has three values. +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class MultiValueSeparatorAttribute : Attribute { + private readonly string? _separator; + /// - /// Specifies a separator for the values of multi-value arguments. + /// Initializes a new instance of the class + /// using white-space as the separator. /// /// /// - /// Normally, you need to supply the argument multiple times to set multiple values, e.g. - /// by using -Sample Value1 -Sample Value2. If you specify the - /// attribute, it allows you to specify multiple values with a single argument by using a - /// separator. - /// - /// - /// There are two ways you can use separators for multi-value arguments: a white-space - /// separator, or an explicit separator string. - /// - /// - /// You enable the use of white-space separators with the - /// constructor. A multi-value argument that allows white-space separators is able to consume - /// multiple values from the command line that follow it. All values that follow the name, up - /// until the next argument name, are considered values for this argument. - /// - /// - /// For example, if you use -Sample Value1 Value2 Value3, all three arguments after - /// -Sample are taken as values. In this case, it's not possible to supply any - /// positional arguments until another named argument has beens supplied. - /// - /// - /// Using white-space separators will not work if the - /// is or if the argument is a multi-value switch argument. - /// - /// - /// Using the constructor, you instead - /// specify an explicit character sequence to be used as a separator. For example, if the - /// separator is set to a comma, you can use -Sample Value1,Value2. + /// A multi-value argument that allows white-space separators is able to consume multiple + /// values from the command line that follow it. All values that follow the name, up until + /// the next argument name, are considered values for this argument. /// + /// + public MultiValueSeparatorAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The separator that separates the values. + /// /// - /// If you specify an explicit separator for a multi-value argument, it will not be - /// possible to use the separator in the individual argument values. There is no way to - /// escape it. + /// If you specify a separator for a multi-value argument, it will not be possible + /// to use the separator character in the individual argument values. There is no way to escape it. + /// + /// + public MultiValueSeparatorAttribute(string separator) + { + _separator = separator; + } + + /// + /// Gets the separator for the values of a multi-value argument. + /// + /// + /// The separator for the argument values, or to indicate that + /// white-space separators are allowed. + /// + /// + /// + /// If you specify a separator for a multi-value argument, it will not be possible + /// to use the separator character in the individual argument values. There is no way to escape it. /// /// - /// Even if the is specified it is still possible to use - /// multiple arguments to specify multiple values. For example, using a comma as the separator, - /// -Sample Value1,Value2 -Sample Value3 will mean the argument "Sample" has three values. + /// A multi-value argument that allows white-space separators is able to consume multiple + /// values from the command line that follow it. All values that follow the name, up until + /// the next argument name, are considered values for this argument. /// /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public class MultiValueSeparatorAttribute : Attribute + public virtual string? Separator { - private readonly string? _separator; - - /// - /// Initializes a new instance of the class - /// using white-space as the separator. - /// - /// - /// - /// A multi-value argument that allows white-space separators is able to consume multiple - /// values from the command line that follow it. All values that follow the name, up until - /// the next argument name, are considered values for this argument. - /// - /// - public MultiValueSeparatorAttribute() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The separator that separates the values. - /// - /// - /// If you specify a separator for a multi-value argument, it will not be possible - /// to use the separator character in the individual argument values. There is no way to escape it. - /// - /// - public MultiValueSeparatorAttribute(string separator) - { - _separator = separator; - } - - /// - /// Gets the separator for the values of a multi-value argument. - /// - /// - /// The separator for the argument values, or to indicate that - /// white-space separators are allowed. - /// - /// - /// - /// If you specify a separator for a multi-value argument, it will not be possible - /// to use the separator character in the individual argument values. There is no way to escape it. - /// - /// - /// A multi-value argument that allows white-space separators is able to consume multiple - /// values from the command line that follow it. All values that follow the name, up until - /// the next argument name, are considered values for this argument. - /// - /// - public virtual string? Separator - { - get { return _separator; } - } + get { return _separator; } } } diff --git a/src/Ookii.CommandLine/NameTransform.cs b/src/Ookii.CommandLine/NameTransform.cs index 5df5e942..b19749dc 100644 --- a/src/Ookii.CommandLine/NameTransform.cs +++ b/src/Ookii.CommandLine/NameTransform.cs @@ -1,42 +1,41 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates how to transform the argument name, subcommand name, or value description if they are +/// not explicitly specified but automatically derived from the member or type name. +/// +/// +/// +/// +/// +/// +public enum NameTransform { /// - /// Indicates how to transform the property, parameter, or method name if an argument doesn't - /// have an explicit name. + /// The names are used without modification. /// - /// - /// - /// - /// - /// - public enum NameTransform - { - /// - /// The names are used without modification. - /// - None, - /// - /// The names are transformed to PascalCase. This removes all underscores, and the first - /// character, and every character after an underscore, is changed to uppercase. The case of - /// other characters is not changed. - /// - PascalCase, - /// - /// The names are transformed to camelCase. Similar to , but the - /// first character will not be uppercase. - /// - CamelCase, - /// - /// The names are transformed to dash-case. This removes leading and trailing underscores, - /// changes all characters to lower-case, replaces underscores with a dash, and reduces - /// consecutive underscores to a single dash. A dash is inserted before previously - /// capitalized letters. - /// - DashCase, - /// - /// The names are transformed to snake_case. Similar to , but uses an - /// underscore instead of a dash. - /// - SnakeCase - } + None, + /// + /// The names are transformed to PascalCase. This removes all underscores, and the first + /// character, and every character after an underscore, is changed to uppercase. The case of + /// other characters is not changed. + /// + PascalCase, + /// + /// The names are transformed to camelCase. Similar to , but the + /// first character will not be uppercase. + /// + CamelCase, + /// + /// The names are transformed to dash-case. This removes leading and trailing underscores, + /// changes all characters to lower-case, replaces underscores with a dash, and reduces + /// consecutive underscores to a single dash. A dash is inserted before previously + /// capitalized letters. + /// + DashCase, + /// + /// The names are transformed to snake_case. Similar to , but uses an + /// underscore instead of a dash. + /// + SnakeCase } diff --git a/src/Ookii.CommandLine/NameTransformExtensions.cs b/src/Ookii.CommandLine/NameTransformExtensions.cs index edd714c3..cf5c4e45 100644 --- a/src/Ookii.CommandLine/NameTransformExtensions.cs +++ b/src/Ookii.CommandLine/NameTransformExtensions.cs @@ -1,127 +1,127 @@ using System; using System.Text; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Extension methods for the enumeration. +/// +/// +public static class NameTransformExtensions { /// - /// Extension methods for the enumeration. + /// Applies the specified transformation to a name. /// - public static class NameTransformExtensions + /// The transformation to apply. + /// The name to transform. + /// + /// An optional suffix to remove from the string before transformation. Only used if + /// is not . + /// + /// The transformed name. + /// + /// is . + /// + public static string Apply(this NameTransform transform, string name, string? suffixToStrip = null) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + int count = name.Length; + if (transform != NameTransform.None && suffixToStrip != null && name.EndsWith(suffixToStrip)) + { + count = name.Length - suffixToStrip.Length; + } + + return transform switch + { + NameTransform.PascalCase => ToPascalOrCamelCase(name, true, count), + NameTransform.CamelCase => ToPascalOrCamelCase(name, false, count), + NameTransform.SnakeCase => ToSnakeOrDashCase(name, '_', count), + NameTransform.DashCase => ToSnakeOrDashCase(name, '-', count), + _ => name, + }; + } + + private static string ToPascalOrCamelCase(string name, bool pascalCase, int count) { - /// - /// Applies the specified transformation to a name. - /// - /// The transformation to apply. - /// The name to transform. - /// - /// An optional suffix to remove from the string before transformation. Only used if - /// is not . - /// - /// The transformed name. - /// - /// is . - /// - public static string Apply(this NameTransform transform, string name, string? suffixToStrip = null) + // Remove any underscores, and the first letter (if pascal case) and any letter after an + // underscore is converted to uppercase. Other letters are unchanged. + var toUpper = pascalCase; + var toLower = !pascalCase; // Only for the first character. + var first = true; + var builder = new StringBuilder(name.Length); + for (int i = 0; i < count; i++) { - if (name == null) + var ch = name[i]; + if (ch == '_') { - throw new ArgumentNullException(nameof(name)); + toUpper = !first || pascalCase; + continue; } - - int count = name.Length; - if (transform != NameTransform.None && suffixToStrip != null && name.EndsWith(suffixToStrip)) + else if (!char.IsLetter(ch)) { - count = name.Length - suffixToStrip.Length; + // Also uppercase/lowercase after non-letters. + builder.Append(ch); + toUpper = pascalCase; + toLower = !pascalCase; + continue; } - return transform switch + first = false; + if (toUpper) { - NameTransform.PascalCase => ToPascalOrCamelCase(name, true, count), - NameTransform.CamelCase => ToPascalOrCamelCase(name, false, count), - NameTransform.SnakeCase => ToSnakeOrDashCase(name, '_', count), - NameTransform.DashCase => ToSnakeOrDashCase(name, '-', count), - _ => name, - }; - } - - private static string ToPascalOrCamelCase(string name, bool pascalCase, int count) - { - // Remove any underscores, and the first letter (if pascal case) and any letter after an - // underscore is converted to uppercase. Other letters are unchanged. - var toUpper = pascalCase; - var toLower = !pascalCase; // Only for the first character. - var first = true; - var builder = new StringBuilder(name.Length); - for (int i = 0; i < count; i++) + builder.Append(char.ToUpperInvariant(ch)); + toUpper = false; + } + else if (toLower) { - var ch = name[i]; - if (ch == '_') - { - toUpper = !first || pascalCase; - continue; - } - else if (!char.IsLetter(ch)) - { - // Also uppercase/lowercase after non-letters. - builder.Append(ch); - toUpper = pascalCase; - toLower = !pascalCase; - continue; - } - - first = false; - if (toUpper) - { - builder.Append(char.ToUpperInvariant(ch)); - toUpper = false; - } - else if (toLower) - { - builder.Append(char.ToLowerInvariant(ch)); - toLower = false; - } - else - { - builder.Append(ch); - } + builder.Append(char.ToLowerInvariant(ch)); + toLower = false; + } + else + { + builder.Append(ch); } - - return builder.ToString(); } - private static string ToSnakeOrDashCase(string name, char separator, int count) + return builder.ToString(); + } + + private static string ToSnakeOrDashCase(string name, char separator, int count) + { + var needSeparator = false; + var first = true; + // Add some leeway to add separators. + var builder = new StringBuilder(name.Length * 2); + for (int i = 0; i < count; ++i) { - var needSeparator = false; - var first = true; - // Add some leeway to add separators. - var builder = new StringBuilder(name.Length * 2); - for (int i = 0; i < count; ++i) + var ch = name[i]; + if (ch == '_') { - var ch = name[i]; - if (ch == '_') - { - needSeparator = !first; - } - else if (!char.IsLetter(ch)) + needSeparator = !first; + } + else if (!char.IsLetter(ch)) + { + needSeparator = false; + first = true; + builder.Append(ch); + } + else + { + if (needSeparator || (char.IsUpper(ch) && !first)) { + builder.Append(separator); needSeparator = false; - first = true; - builder.Append(ch); } - else - { - if (needSeparator || (char.IsUpper(ch) && !first)) - { - builder.Append(separator); - needSeparator = false; - } - builder.Append(char.ToLowerInvariant(ch)); - first = false; - } + builder.Append(char.ToLowerInvariant(ch)); + first = false; } - - return builder.ToString(); } + + return builder.ToString(); } } diff --git a/src/Ookii.CommandLine/NativeMethods.cs b/src/Ookii.CommandLine/NativeMethods.cs index 48975e51..50b02b76 100644 --- a/src/Ookii.CommandLine/NativeMethods.cs +++ b/src/Ookii.CommandLine/NativeMethods.cs @@ -2,95 +2,114 @@ using System; using System.Runtime.InteropServices; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +static partial class NativeMethods { - static class NativeMethods - { - static readonly IntPtr INVALID_HANDLE_VALUE = new(-1); + static readonly IntPtr INVALID_HANDLE_VALUE = new(-1); - public static ConsoleModes? EnableVirtualTerminalSequences(StandardStream stream, bool enable) + public static (bool, ConsoleModes?) EnableVirtualTerminalSequences(StandardStream stream, bool enable) + { + if (stream == StandardStream.Input) { - if (stream == StandardStream.Input) - { - throw new ArgumentException(Properties.Resources.InvalidStandardStream, nameof(stream)); - } - - var handle = GetStandardHandle(stream); - if (handle == INVALID_HANDLE_VALUE) - { - return null; - } - - if (!GetConsoleMode(handle, out ConsoleModes mode)) - { - return null; - } - - var oldMode = mode; - if (enable) - { - mode |= ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; - } - else - { - mode &= ~ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; - } - - if (!SetConsoleMode(handle, mode)) - { - return null; - } - - return oldMode; + throw new ArgumentException(Properties.Resources.InvalidStandardStream, nameof(stream)); } - public static IntPtr GetStandardHandle(StandardStream stream) + var handle = GetStandardHandle(stream); + if (handle == INVALID_HANDLE_VALUE) { - 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); + return (false, null); } - [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); + if (!GetConsoleMode(handle, out ConsoleModes mode)) + { + return (false, null); + } - [DllImport("kernel32.dll", SetLastError = true)] - static extern IntPtr GetStdHandle(StandardHandle nStdHandle); + var oldMode = mode; + if (enable) + { + mode |= ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + else + { + mode &= ~ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } - [Flags] - public enum ConsoleModes : uint + if (oldMode == mode) { - 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, - - 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 + return (true, null); } - private enum StandardHandle + if (!SetConsoleMode(handle, mode)) { - STD_OUTPUT_HANDLE = -11, - STD_INPUT_HANDLE = -10, - STD_ERROR_HANDLE = -12, + 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/NullableConverterWrapper.cs b/src/Ookii.CommandLine/NullableConverterWrapper.cs deleted file mode 100644 index 3bcc5009..00000000 --- a/src/Ookii.CommandLine/NullableConverterWrapper.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; - -namespace Ookii.CommandLine -{ - // Unfortunately the regular NullableConverter can't be used for this because it doesn't allow - // the use of a custom TypeConverter. It otherwise behaves the same (converts an empty string - // to null). - internal class NullableConverterWrapper : TypeConverter - { - private readonly Type _underlyingType; - private readonly TypeConverter _baseConverter; - - public NullableConverterWrapper(Type underlyingType, TypeConverter baseConverter) - { - _underlyingType = underlyingType; - _baseConverter = baseConverter; - } - - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - => sourceType == typeof(string) || sourceType == _underlyingType || base.CanConvertFrom(context, sourceType); - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - if (value == null || value.GetType() == _underlyingType) - { - return value; - } - - if (value is string stringValue && stringValue.Length == 0) - { - return null; - } - - return _baseConverter.ConvertFrom(context, culture, value); - } - } -} diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 8a2bbfa1..88e97b23 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -1,46 +1,74 @@  - net6.0;netstandard2.0;netstandard2.1 + net7.0;net6.0;netstandard2.0;netstandard2.1 enable - 9.0 + 11.0 True True + True MIT - https://github.com/SvenGroot/ookii.commandline - https://github.com/SvenGroot/ookii.commandline + https://github.com/SvenGroot/Ookii.CommandLine + https://github.com/SvenGroot/Ookii.CommandLine git true true ookii.snk false - Ookii.CommandLine is a powerful command line parsing library for .Net applications. + en-US + Ookii.CommandLine + Ookii.CommandLine is a powerful, flexible and highly customizable command line argument parsing +library for .Net applications. - Easily define arguments by creating a class with properties. - Create applications with multiple subcommands. - Generate fully customizable usage help. -- Supports PowerShell-like and POSIX-like parsing rules. +- Supports PowerShell-like and POSIX-like parsing rules. +- Trim-friendly command line arguments parsing parser parse argument args console - This version contains breaking changes compared to version 2.x. For details, please view: https://www.ookii.org/Link/CommandLineVersionHistory + This version contains breaking changes compared to version 2.x and 3.x. For details, please view: https://www.ookii.org/Link/CommandLineVersionHistory true snupkg icon.png + true + true + PackageReadme.md true - - CS1574 + + CS1574 + + + + + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Ookii.CommandLine/PackageReadme.md b/src/Ookii.CommandLine/PackageReadme.md new file mode 100644 index 00000000..9bf2bc04 --- /dev/null +++ b/src/Ookii.CommandLine/PackageReadme.md @@ -0,0 +1,60 @@ +# Ookii.CommandLine + +Ookii.CommandLine is a powerful, flexible and highly customizable command line argument parsing +library for .Net applications. + +- Easily define arguments by creating a class with properties. +- Create applications with multiple subcommands. +- Generate fully customizable usage help. +- Supports PowerShell-like and POSIX-like parsing rules. +- Trim-friendly + +Two styles of command line parsing rules are supported: the default mode uses rules similar to those +used by PowerShell, and the alternative long/short mode uses a style influenced by POSIX +conventions, where arguments have separate long and short names with different prefixes. Many +aspects of the parsing rules are configurable. + +To determine which arguments are accepted, you create a class, with properties and methods that +define the arguments. Attributes are used to specify names, create required or positional arguments, +and to specify descriptions for use in the generated usage help. + +For example, the following class defines four arguments: a required positional argument, an optional +positional argument, a named-only argument, and a switch argument (sometimes also called a flag): + +```csharp +[GeneratedParser] +partial class MyArguments +{ + [CommandLineArgument(IsPositional = true)] + [Description("A required positional argument.")] + public required string Required { get; set; } + + [CommandLineArgument(IsPositional = true)] + [Description("An optional positional argument.")] + public int Optional { get; set; } = 42; + + [CommandLineArgument] + [Description("An argument that can only be supplied by name.")] + public DateTime Named { get; set; } + + [CommandLineArgument] + [Description("A switch argument, which doesn't require a value.")] + public bool Switch { get; set; } +} +``` + +Each argument has a different type that determines the kinds of values it can accept. + +> If you are using an older version of .Net where the `required` keyword is not available, you can +> use `[CommandLineArgument(IsRequired = true)]` to create a required argument instead. + +To parse these arguments, all you have to do is add the following line to your `Main` method: + +```csharp +var arguments = MyArguments.Parse(); +``` + +In addition, Ookii.CommandLine can be used to create applications that have multiple subcommands, +each with their own arguments. + +For more information, including a tutorial and samples, see the [full documentation on GitHub](https://github.com/SvenGroot/Ookii.CommandLine). diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 80fdfc06..8d2615f7 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -1,5 +1,4 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Commands; using Ookii.CommandLine.Terminal; using System; using System.Collections.Generic; @@ -7,542 +6,818 @@ using System.Globalization; using System.IO; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides options that control parsing behavior. +/// +/// +/// +/// Several options can also be specified using the +/// attribute on the type defining the arguments. If the option is set in both in the +/// attribute and here, the value from the class will override the +/// value from the attribute. +/// +/// +/// +/// +/// +/// +/// +/// +public class ParseOptions { + private CultureInfo? _culture; + private UsageWriter? _usageWriter; + private LocalizedStringProvider? _stringProvider; + + /// + /// Gets or sets the culture used to convert command line argument values from their string + /// representation to the argument type. + /// + /// + /// The culture used to convert command line argument values. The default value is + /// . + /// + /// +#if NET6_0_OR_GREATER + [AllowNull] +#endif + public CultureInfo Culture + { + get => _culture ?? CultureInfo.InvariantCulture; + set => _culture = value; + } + + /// + /// Gets or sets a value that indicates the command line argument parsing rules to use. + /// + /// + /// One of the values of the enumeration, or + /// to use the value from the attribute, or if that + /// attribute is not present, . The default value is + /// . + /// + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + public ParsingMode? Mode { get; set; } + + /// + /// Gets a value that indicates the command line argument parsing rules to use. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public ParsingMode ModeOrDefault => Mode ?? ParsingMode.Default; + /// - /// Provides options for the - /// method and the constructor. + /// Gets or sets a value that indicates whether the options follow POSIX conventions. /// + /// + /// if the options follow POSIX conventions; otherwise, + /// . + /// /// /// - /// Several options can also be specified using the - /// attribute on the type defining the arguments. If the option is set in both in the - /// attribute and here, the value from the class will override the - /// value from the attribute. + /// This property is provided as a convenient way to set a number of related properties that + /// together indicate the parser is using POSIX conventions. POSIX conventions in this case + /// means that parsing uses long/short mode, argument names are case sensitive, and argument + /// names and value descriptions use dash case (e.g. "argument-name"). + /// + /// + /// Setting this property to is equivalent to setting the + /// property to , the + /// property to , + /// the property to , + /// and the property to . + /// + /// + /// This property will only return if the above properties are the + /// indicated values, except that can be any + /// case-sensitive comparison. It will return for any other + /// combination of values, not just the ones indicated below. + /// + /// + /// Setting this property to is equivalent to setting the + /// property to , the + /// property to , + /// the property to , + /// and the property to . /// /// - public class ParseOptions + /// + /// + public virtual bool IsPosix { - private UsageWriter? _usageWriter; - private LocalizedStringProvider? _stringProvider; - - /// - /// Gets or sets the culture used to convert command line argument values from their string representation to the argument type. - /// - /// - /// The culture used to convert command line argument values from their string representation to the argument type, or - /// to use . The default value is - /// - /// - public CultureInfo? Culture { get; set; } - - /// - /// Gets or sets a value that indicates the command line argument parsing rules to use. - /// - /// - /// One of the values of the enumeration, or - /// to use the value from the attribute, or if that - /// attribute is not present, . The default value is - /// . - /// - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - public ParsingMode? Mode { get; set; } - - /// - /// Gets or sets a value that indicates how names are created for arguments that don't have - /// an explicit name. - /// - /// - /// One of the values of the enumeration, or - /// to use the value from the attribute, or if that - /// attribute is not present, . The default value is - /// . - /// - /// - /// - /// If an argument doesn't have the - /// property set (or doesn't have an attribute for - /// constructor parameters), the argument name is determined by taking the name of the - /// property, constructor parameter, or method that defines it, and applying the specified - /// transform. - /// - /// - /// The name transform will also be applied to the names of the automatically added - /// help and version attributes. - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - /// - /// - public NameTransform? ArgumentNameTransform { get; set; } - - /// - /// Gets or sets the argument name prefixes to use when parsing the arguments. - /// - /// - /// The named argument switches, or to use the values from the - /// attribute, or if not set, the default prefixes for - /// the current platform as returned by the - /// method. The default value is . - /// - /// - /// - /// If the parsing mode is set to , either using the - /// property or the attribute, - /// this property sets the short argument name prefixes. Use the - /// property to set the argument prefix for long names. - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - - public IEnumerable? ArgumentNamePrefixes { get; set; } - - /// - /// Gets or sets the argument name prefix to use for long argument names. - /// - /// - /// The long argument prefix, or to use the value from the - /// attribute, or if not set, the default prefix from - /// the constant. The default - /// value is . - /// - /// - /// - /// This property is only used if the if the parsing mode is set to , - /// either using the property or the - /// attribute - /// - /// - /// Use the to specify the prefixes for short argument - /// names. - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - public string? LongArgumentNamePrefix { get; set; } - - /// - /// Gets or set the to use to compare argument names. - /// - /// - /// The to use to compare the names of named arguments, or - /// to use the one determined using the - /// property, or if the is not present, . - /// The default value is . - /// - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - public IComparer? ArgumentNameComparer { get; set; } - - /// - /// Gets or sets the used to print error information if argument - /// parsing fails. - /// - /// - /// If argument parsing is successful, nothing will be written. - /// - /// - /// The used to print error information, or - /// to print to a for the standard error stream - /// (). The default value is . - /// - public TextWriter? Error { get; set; } - - /// - /// Gets or sets a value indicating whether duplicate arguments are allowed. - /// - /// - /// One of the values of the enumeration, or - /// to use the value from the attribute, or if that - /// attribute is not present, . The default value is - /// . - /// - /// - /// - /// If set to , supplying a non-multi-value argument more - /// than once will cause an exception. If set to , the - /// last value supplied will be used. - /// - /// - /// If set to , the - /// method, the static method and - /// the class will print a warning to the - /// stream when a duplicate argument is found. If you are not using these methods, - /// is identical to and no - /// warning is displayed. - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - public ErrorMode? DuplicateArguments { get; set; } - - /// - /// Gets or sets a value indicating whether the value of arguments may be separated from the name by white space. - /// - /// - /// if white space is allowed to separate an argument name and its - /// value; if only the is allowed, - /// or to use the value from the - /// property, or if the is not present, the default - /// option which is . The default value is . - /// - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - public bool? AllowWhiteSpaceValueSeparator { get; set; } - - /// - /// Gets or sets the character used to separate the name and the value of an argument. - /// - /// - /// The character used to separate the name and the value of an argument, or - /// to use the value from the attribute, or if that - /// is not present, the - /// constant, a colon (:). The default value is . - /// - /// - /// - /// This character is used to separate the name and the value if both are provided as - /// a single argument to the application, e.g. -sample:value if the default value is used. - /// - /// - /// The character chosen here cannot be used in the name of any parameter. Therefore, - /// it's usually best to choose a non-alphanumeric value such as the colon or equals sign. - /// The character can appear in argument values (e.g. -sample:foo:bar is fine, in which - /// case the value is "foo:bar"). - /// - /// - /// Do not pick a white-space character as the separator. Doing this only works if the - /// whitespace character is part of the argument, which usually means it needs to be - /// quoted or escaped when invoking your application. Instead, use the - /// property to control whether white space - /// is allowed as a separator. - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - public char? NameValueSeparator { get; set; } - - /// - /// Gets or sets a value that indicates a help argument will be automatically added. - /// - /// - /// to automatically create a help argument; - /// to not create one, or to use the value from the - /// attribute, or if that is not present, . The default value is - /// . - /// - /// - /// - /// If this property is , the - /// will automatically add an argument with the name "Help". If using , - /// this argument will have the short name "?" and a short alias "h"; otherwise, it - /// will have the aliases "?" and "h". When supplied, this argument will cancel parsing - /// and cause usage help to be printed. - /// - /// - /// If you already have an argument conflicting with the names or aliases above, the - /// automatic help argument will not be created even if this property is - /// . - /// - /// - /// The name, aliases and description can be customized by using a custom . - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - /// - /// - public bool? AutoHelpArgument { get; set; } - - /// - /// Gets or sets a value that indicates a version argument will be automatically added. - /// - /// - /// to automatically create a version argument; - /// to not create one, or to use the value from the - /// property, or if the - /// is not present, . - /// The default value is . - /// - /// - /// - /// If this property is , the - /// will automatically add an argument with the name "Version". When supplied, this - /// argument will write version information to the console and cancel parsing, without - /// showing usage help. - /// - /// - /// If you already have an argument named "Version", the automatic version argument - /// will not be created even if this property is . - /// - /// - /// The name and description can be customized by using a custom . - /// - /// - /// If not , this property overrides the value of the - /// property. - /// - /// - /// - /// - public bool? AutoVersionArgument { get; set; } = true; - - /// - /// Gets or sets the color applied to error messages. - /// - /// - /// The virtual terminal sequence for a color. The default value is - /// . - /// - /// - /// - /// The color will only be used if the property is - /// ; otherwise, it will be replaced with an empty string. - /// - /// - /// If the string contains anything other than virtual terminal sequences, those parts - /// will be included in the output, but only when the property is - /// . - /// - /// - /// After the error message, the value of the - /// property will be written to undo the color change. - /// - /// - public string ErrorColor { get; set; } = TextFormat.ForegroundRed; - - /// - /// Gets or sets the color applied to warning messages. - /// - /// - /// The virtual terminal sequence for a color. The default value is - /// . - /// - /// - /// - /// The color will only be used if the property is - /// ; otherwise, it will be replaced with an empty string. - /// - /// - /// This color is used for the warning emitted if the - /// property is . - /// - /// - /// If the string contains anything other than virtual terminal sequences, those parts - /// will be included in the output, but only when the property is - /// . - /// - /// - /// After the warning message, the value of the - /// property will be written to undo the color change. - /// - /// - public string WarningColor { get; set; } = TextFormat.ForegroundYellow; - - /// - /// Gets or sets a value that indicates whether error messages should use color. - /// - /// - /// to enable color output; to disable - /// color output; or to enable it if the error output supports it. - /// - /// - /// - /// If this property is and the property is - /// , the - /// method, the - /// method and the class will determine if color is supported - /// using the method for the standard error - /// stream. - /// - /// - /// If this property is set to explicitly, virtual terminal - /// sequences may be included in the output even if it's not supported, which may lead to - /// garbage characters appearing in the output. - /// - /// - public bool? UseErrorColor { get; set; } - - /// - /// Gets or sets the implementation to use to get - /// strings for error messages and usage help. - /// - /// - /// An instance of a class inheriting from the class. - /// The default value is an instance of the class - /// itself. - /// - /// - /// - /// Set this property if you want to customize or localize error messages or usage help - /// strings. - /// - /// - /// - public LocalizedStringProvider StringProvider + get => Mode == ParsingMode.LongShort && ArgumentNameComparisonOrDefault.IsCaseSensitive() && + ArgumentNameTransform == NameTransform.DashCase && ValueDescriptionTransform == NameTransform.DashCase; + set { - get => _stringProvider ??= new LocalizedStringProvider(); - set => _stringProvider = value; + if (value) + { + Mode = ParsingMode.LongShort; + ArgumentNameComparison = StringComparison.InvariantCulture; + ArgumentNameTransform = NameTransform.DashCase; + ValueDescriptionTransform = NameTransform.DashCase; + } + else + { + Mode = ParsingMode.Default; + ArgumentNameComparison = StringComparison.OrdinalIgnoreCase; + ArgumentNameTransform = NameTransform.None; + ValueDescriptionTransform = NameTransform.None; + } } + } + + /// + /// Gets or sets a value that indicates how names are created for arguments that don't have + /// an explicit name. + /// + /// + /// One of the values of the enumeration, or + /// to use the value from the attribute, or if that + /// attribute is not present, . The default value is + /// . + /// + /// + /// + /// If an argument doesn't have the + /// property set, the argument name is determined by taking the name of the property or + /// method that defines it, and applying the specified transformation. + /// + /// + /// The name transform will also be applied to the names of the automatically added + /// help and version attributes. + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + /// + /// + public NameTransform? ArgumentNameTransform { get; set; } + + /// + /// Gets a value that indicates how names are created for arguments that don't have an explicit + /// name. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public NameTransform ArgumentNameTransformOrDefault => ArgumentNameTransform ?? NameTransform.None; + + /// + /// Gets or sets the argument name prefixes to use when parsing the arguments. + /// + /// + /// The named argument switches, or to use the values from the + /// attribute, or if not set, the default prefixes for + /// the current platform as returned by the + /// method. The default value is . + /// + /// + /// + /// If the parsing mode is set to , + /// either using the property or the + /// property, this property sets the short argument name prefixes. Use the + /// property to set the argument prefix for long names. + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + + public IEnumerable? ArgumentNamePrefixes { get; set; } + + /// + /// Gets the argument name prefixes to use when parsing the arguments. + /// + /// + /// The value of the property, or the return value of the + /// method if that property + /// is + /// + public IEnumerable ArgumentNamePrefixesOrDefault => ArgumentNamePrefixes ?? CommandLineParser.GetDefaultArgumentNamePrefixes(); + + /// + /// Gets or sets the argument name prefix to use for long argument names. + /// + /// + /// The long argument prefix, or to use the value from the + /// attribute, or if not set, the default prefix from + /// the constant. The default + /// value is . + /// + /// + /// + /// This property is only used if the if the parsing mode is set to , + /// either using the property or the + /// attribute + /// + /// + /// Use the to specify the prefixes for short argument + /// names. + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + public string? LongArgumentNamePrefix { get; set; } + + /// + /// Gets the argument name prefix to use for long argument names. + /// + /// + /// The value of the property, or the value of the + /// constant if that property + /// is + /// + public string LongArgumentNamePrefixOrDefault => LongArgumentNamePrefix ?? CommandLineParser.DefaultLongArgumentNamePrefix; + + /// + /// Gets or set the type of string comparison to use for argument names. + /// + /// + /// One of the values of the enumeration, or + /// to use the one determined using the + /// property, or if the + /// is not present, + /// . The default value is + /// . + /// + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + public StringComparison? ArgumentNameComparison { get; set; } + + /// + /// Gets the type of string comparison to use for argument names. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public StringComparison ArgumentNameComparisonOrDefault => ArgumentNameComparison ?? StringComparison.OrdinalIgnoreCase; + + + /// + /// Gets or sets the used to print error information if argument + /// parsing fails. + /// + /// + /// The used to print error information, or + /// to print to a for the standard error stream + /// (). The default value is . + /// + /// + /// + /// Only the parsing methods that automatically handle errors will use this property. + /// + /// + /// If argument parsing is successful, nothing will be written. + /// + /// + /// + /// + /// + public TextWriter? Error { get; set; } + + /// + /// Gets or sets a value indicating whether duplicate arguments are allowed. + /// + /// + /// One of the values of the enumeration, or + /// to use the value from the attribute, or if that + /// attribute is not present, . The default value is + /// . + /// + /// + /// + /// If set to , supplying a non-multi-value argument more + /// than once will cause an exception. If set to , the + /// last value supplied will be used. + /// + /// + /// If set to , the + /// method, the static + /// method, the generated + /// method and the class will print a warning to the stream + /// indicated by the property when a duplicate argument is found. If you + /// are not using these methods, is + /// identical to , and no warning is + /// displayed. + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + public ErrorMode? DuplicateArguments { get; set; } + + /// + /// Gets a value indicating whether duplicate arguments are allowed. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public ErrorMode DuplicateArgumentsOrDefault => DuplicateArguments ?? ErrorMode.Error; + + /// + /// Gets or sets a value indicating whether the value of arguments may be separated from the name by white space. + /// + /// + /// if white space is allowed to separate an argument name and its + /// value; if only the are allowed, + /// or to use the value from the + /// property, or if the is not present, the default + /// option which is . The default value is . + /// + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + public bool? AllowWhiteSpaceValueSeparator { get; set; } + + /// + /// Gets a value indicating whether the value of arguments may be separated from the name by + /// white space. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public bool AllowWhiteSpaceValueSeparatorOrDefault => AllowWhiteSpaceValueSeparator ?? true; + + /// + /// Gets or sets the characters used to separate the name and the value of an argument. + /// + /// + /// The character used to separate the name and the value of an argument, or + /// to use the value from the attribute, or if that + /// is not present, the values returned by the + /// method, which are a colon (:) and an equals sign (=). The default value is . + /// + /// + /// + /// These characters are used to separate the name and the value if both are provided as + /// a single argument to the application, e.g. -sample:value or -sample=value + /// if the default value is used. + /// + /// + /// The characters chosen here cannot be used in the name of any parameter. Therefore, + /// it's usually best to choose a non-alphanumeric value such as the colon or equals sign. + /// The characters can appear in argument values (e.g. -sample:foo:bar is fine, in\ + /// which case the value is "foo:bar"). + /// + /// + /// Do not pick a white-space character as the separator. Doing this only works if the + /// white-space character is part of the argument token, which usually means it needs to be + /// quoted or escaped when invoking your application. Instead, use the + /// property to control whether white space + /// is allowed as a separator. + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + public IEnumerable? NameValueSeparators { get; set; } + + /// + /// Gets the characters used to separate the name and the value of an argument. + /// + /// + /// The value of the property, or the return value of the + /// method if that property is + /// . + /// + public IEnumerable NameValueSeparatorsOrDefault => NameValueSeparators ?? CommandLineParser.GetDefaultNameValueSeparators(); + + /// + /// Gets or sets a value that indicates a help argument will be automatically added. + /// + /// + /// to automatically create a help argument; + /// to not create one, or to use the value from the + /// attribute, or if that is not present, . The default value is + /// . + /// + /// + /// + /// If this property is , the + /// will automatically add an argument with the name "Help". If using , + /// this argument will have the short name "?" and a short alias "h"; otherwise, it + /// will have the aliases "?" and "h". When supplied, this argument will cancel parsing + /// and cause usage help to be printed. + /// + /// + /// If you already have an argument conflicting with the names or aliases above, the + /// automatic help argument will not be created even if this property is + /// . + /// + /// + /// The name, aliases and description can be customized by using a custom . + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + /// + /// + public bool? AutoHelpArgument { get; set; } + + /// + /// Gets a value that indicates a help argument will be automatically added. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public bool AutoHelpArgumentOrDefault => AutoHelpArgument ?? true; + + /// + /// Gets or sets a value that indicates a version argument will be automatically added. + /// + /// + /// to automatically create a version argument; + /// to not create one, or to use the value from the + /// property, or if the + /// is not present, . + /// The default value is . + /// + /// + /// + /// If this property is , the + /// will automatically add an argument with the name "Version". When supplied, this + /// argument will write version information to the console and cancel parsing, without + /// showing usage help. + /// + /// + /// If you already have an argument named "Version", the automatic version argument + /// will not be created even if this property is . + /// + /// + /// The name and description can be customized by using a custom . + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + /// + /// + public bool? AutoVersionArgument { get; set; } + + /// + /// Gets a value that indicates a version argument will be automatically added. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public bool AutoVersionArgumentOrDefault => AutoVersionArgument ?? true; + - /// - /// Gets or sets a value that indicates how usage is shown after a parsing error occurred. - /// - /// - /// One of the values of the enumeration. The default value - /// is . - /// - /// - /// - /// If the value of this property is not , the - /// method, the - /// method and the - /// class will write the message returned by the - /// method instead of usage help. - /// - /// - public UsageHelpRequest ShowUsageOnError { get; set; } - - /// - /// Gets or sets a dictionary containing default value descriptions for types. - /// - /// - /// A dictionary containing default value descriptions for types, or . - /// - /// - /// - /// The value description is a short, typically one-word description that indicates the - /// type of value that the user should supply. It is not the long description used to - /// describe the purpose of the argument. - /// - /// - /// If an argument doesn't have the - /// property set or the attribute applied, the - /// value description will be determined by first checking this dictionary. If the type - /// of the argument isn't in the dictionary, the type name is used, applying the - /// transformation specified by the property. - /// - /// - /// - public IDictionary? DefaultValueDescriptions { get; set; } - - /// - /// Gets or sets a value that indicates how value descriptions derived from type names - /// are transformed. - /// - /// - /// One of the members of the enumeration, or - /// to use the value from the attribute, or if that is - /// not present, . The default value is . - /// - /// - /// - /// This property has no effect on explicit value description specified with the - /// property, the - /// attribute, or the property. - /// - /// - /// If not , this property overrides the - /// property. - /// - /// - public NameTransform? ValueDescriptionTransform { get; set; } - - /// - /// Gets or sets the to use to create usage help. - /// - /// - /// An instance of the class. - /// + /// + /// Gets or sets a value that indicates whether unique prefixes of an argument are automatically + /// used as aliases. + /// + /// + /// to automatically use unique prefixes of an argument as aliases for + /// that argument; to not have automatic prefixes; otherwise, + /// to use the value from the + /// property, or if the attribute is not present, + /// . + /// + /// + /// + /// If this property is , the class + /// will consider any prefix that uniquely identifies an argument by its name or one of its + /// explicit aliases as an alias for that argument. For example, given two arguments "Port" + /// and "Protocol", "Po" and "Por" would be an alias for "Port", and "Pr" an alias for + /// "Protocol" (as well as "Pro", "Prot", "Proto", etc.). "P" would not be an alias because it + /// doesn't uniquely identify a single argument. + /// + /// + /// When using , this only applies to long names. Explicit + /// aliases set with the take precedence over automatic aliases. + /// Automatic prefix aliases are not shown in the usage help. + /// + /// + /// This behavior is enabled unless explicitly disabled here or using the + /// property. + /// + /// + /// If not , this property overrides the value of the + /// property. + /// + /// + public bool? AutoPrefixAliases { get; set; } + + /// + /// Gets a value that indicates whether unique prefixes of an argument are automatically used as + /// aliases. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public bool AutoPrefixAliasesOrDefault => AutoPrefixAliases ?? true; + + /// + /// Gets or sets the color applied to error messages. + /// + /// + /// The virtual terminal sequence for a color. The default value is + /// . + /// + /// + /// + /// Only the parsing methods that automatically handle errors will use this property. + /// + /// + /// The color will only be used if the property is + /// ; otherwise, it will be replaced with an empty string. + /// + /// + /// After the error message, the value of the + /// property will be written to undo the color change. + /// + /// + /// + /// + /// + public TextFormat ErrorColor { get; set; } = TextFormat.ForegroundRed; + + /// + /// Gets or sets the color applied to warning messages. + /// + /// + /// The virtual terminal sequence for a color. The default value is + /// . + /// + /// + /// + /// Only the parsing methods that automatically handle errors will use this property. + /// + /// + /// The color will only be used if the property is + /// ; otherwise, it will be replaced with an empty string. + /// + /// + /// This color is used for the warning emitted if the + /// property is . + /// + /// + /// After the warning message, the value of the + /// property will be written to undo the color change. + /// + /// + /// + /// + /// + public TextFormat WarningColor { get; set; } = TextFormat.ForegroundYellow; + + /// + /// Gets or sets a value that indicates whether error messages should use color. + /// + /// + /// to enable color output; to disable + /// color output; or to enable it if the error output supports it. + /// + /// + /// + /// Only the parsing methods that automatically handle errors will use this property. + /// + /// + /// If this property is and the property is + /// , color will be used if the standard error stream supports it, as + /// determined by the method. + /// + /// + /// If this property is set to explicitly, virtual terminal + /// sequences may be included in the output even if it's not supported, which may lead to + /// garbage characters appearing in the output. + /// + /// + /// + /// + /// + public bool? UseErrorColor { get; set; } + + /// + /// Gets or sets the implementation to use to get + /// strings for error messages and usage help. + /// + /// + /// An instance of a class inheriting from the class. + /// The default value is an instance of the class + /// itself. + /// + /// + /// + /// Set this property if you want to customize or localize error messages or usage help + /// strings. + /// + /// + /// #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - [AllowNull] + [AllowNull] #endif - public UsageWriter UsageWriter - { - get => _usageWriter ??= new UsageWriter(); - set => _usageWriter = value; - } + public LocalizedStringProvider StringProvider + { + get => _stringProvider ??= new LocalizedStringProvider(); + set => _stringProvider = value; + } - /// - /// Merges the options in this instance with the options from the - /// attribute. - /// - /// The . - /// - /// is . - /// - /// - /// - /// For all properties that have an equivalent in the , - /// class, if the property in this instance is , it will be set to - /// the value from the class. - /// - /// - public void Merge(ParseOptionsAttribute attribute) - { - if (attribute == null) - { - throw new ArgumentNullException(nameof(attribute)); - } + /// + /// Gets or sets a value that indicates how usage is shown after a parsing error occurred. + /// + /// + /// One of the values of the enumeration. The default value + /// is . + /// + /// + /// + /// Only the parsing methods that automatically handle errors will use this property. + /// + /// + /// If the value of this property is not , + /// the message returned by the + /// method is written instead of the omitted parts of the usage help. + /// + /// + /// + /// + /// + public UsageHelpRequest ShowUsageOnError { get; set; } - Mode ??= attribute.Mode; - ArgumentNameTransform ??= attribute.ArgumentNameTransform; - ArgumentNamePrefixes ??= attribute.ArgumentNamePrefixes; - LongArgumentNamePrefix ??= attribute.LongArgumentNamePrefix; - ArgumentNameComparer ??= attribute.GetStringComparer(); - DuplicateArguments ??= attribute.DuplicateArguments; - AllowWhiteSpaceValueSeparator ??= attribute.AllowWhiteSpaceValueSeparator; - NameValueSeparator ??= attribute.NameValueSeparator; - AutoHelpArgument ??= attribute.AutoHelpArgument; - AutoVersionArgument ??= attribute.AutoVersionArgument; - ValueDescriptionTransform ??= attribute.ValueDescriptionTransform; - } + /// + /// Gets or sets a dictionary containing default value descriptions for types. + /// + /// + /// A dictionary containing default value descriptions for types, or . + /// + /// + /// + /// The value description is a short, typically one-word description that indicates the + /// type of value that the user should supply. It is not the long description used to + /// describe the purpose of the argument. + /// + /// + /// If an argument doesn't have the attribute + /// applied, the value description will be determined by first checking this dictionary. + /// If the type of the argument isn't in the dictionary, the type name is used, applying + /// the transformation specified by the property. + /// + /// + /// + public IDictionary? DefaultValueDescriptions { get; set; } + + /// + /// Gets or sets a value that indicates how value descriptions derived from type names + /// are transformed. + /// + /// + /// One of the members of the enumeration, or + /// to use the value from the attribute, or if that is + /// not present, . The default value is . + /// + /// + /// + /// This property has no effect on explicit value description specified with the + /// attribute or the + /// property. + /// + /// + /// If not , this property overrides the + /// property. + /// + /// + public NameTransform? ValueDescriptionTransform { get; set; } + + /// + /// Gets a value that indicates how value descriptions derived from type names are transformed. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public NameTransform ValueDescriptionTransformOrDefault => ValueDescriptionTransform ?? NameTransform.None; + + /// + /// Gets or sets a value that indicates whether the class + /// will use reflection even if the command line arguments type has the + /// . + /// + /// + /// to force the use of reflection when the arguments class has the + /// attribute; otherwise, . The + /// default value is . + /// + /// + /// + /// This property only applies when you manually construct an instance of the + /// or class, or use one + /// of the static + /// methods. If you use the generated static or + /// interface methods on the command line arguments type, + /// the generated parser is used regardless of the value of this property. + /// + /// + public bool ForceReflection { get; set; } = ForceReflectionDefault; - internal VirtualTerminalSupport? EnableErrorColor() + // Used by the tests so we can get coverage of the default options path while not causing + // exceptions. + internal static bool ForceReflectionDefault { get; set; } + + /// + /// Gets or sets the to use to create usage help. + /// + /// + /// An instance of a class inheriting from the class. + /// The default value is an instance of the class + /// itself. + /// +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + [AllowNull] +#endif + public UsageWriter UsageWriter + { + get => _usageWriter ??= new UsageWriter(); + set => _usageWriter = value; + } + + /// + /// Merges the options in this instance with the options from the + /// attribute. + /// + /// The . + /// + /// is . + /// + /// + /// + /// For all properties that have an equivalent in the , + /// class, if the property in this instance is , it will be set to + /// the value from the class. + /// + /// + public void Merge(ParseOptionsAttribute attribute) + { + if (attribute == null) { - if (Error == null && UseErrorColor == null) - { - var support = VirtualTerminal.EnableColor(StandardStream.Error); - UseErrorColor = support.IsSupported; - return support; - } + throw new ArgumentNullException(nameof(attribute)); + } + + Mode ??= attribute.Mode; + ArgumentNameTransform ??= attribute.ArgumentNameTransform; + ArgumentNamePrefixes ??= attribute.ArgumentNamePrefixes; + LongArgumentNamePrefix ??= attribute.LongArgumentNamePrefix; + ArgumentNameComparison ??= attribute.GetStringComparison(); + DuplicateArguments ??= attribute.DuplicateArguments; + AllowWhiteSpaceValueSeparator ??= attribute.AllowWhiteSpaceValueSeparator; + NameValueSeparators ??= attribute.NameValueSeparators; + AutoHelpArgument ??= attribute.AutoHelpArgument; + AutoVersionArgument ??= attribute.AutoVersionArgument; + AutoPrefixAliases ??= attribute.AutoPrefixAliases; + ValueDescriptionTransform ??= attribute.ValueDescriptionTransform; + } - return null; + internal VirtualTerminalSupport? EnableErrorColor() + { + if (Error == null && UseErrorColor == null) + { + var support = VirtualTerminal.EnableColor(StandardStream.Error); + UseErrorColor = support.IsSupported; + return support; } + + return null; } } diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index c12d8e56..c28e0420 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -1,325 +1,424 @@ using System; -using System.Collections.Generic; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides options that alter parsing behavior for the class that the attribute is applied +/// to. +/// +/// +/// +/// Options for parsing command line arguments can be supplied either using this attribute, or +/// by using the class. Options set using the +/// class will override the equivalent options set in the +/// attribute. +/// +/// +/// For subcommands, options set using the attribute apply +/// only to the command with the attribute. Apply the attribute to a common base class to set +/// options for multiple commands, or use the class, which +/// derives from the class, to set options for all commands. +/// +/// +/// If this is attribute is not present, the default options, or those set in the +/// class, will be used. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public class ParseOptionsAttribute : Attribute { /// - /// Provides options that alter parsing behavior for the class that the attribute is applied - /// to. + /// Gets or sets a value that indicates the command line argument parsing rules to use. /// + /// + /// The to use. The default is . + /// /// /// - /// Options can be provided in several ways; you can change the properties of the - /// class, you can use the class, - /// or you can use the attribute. + /// This value can be overridden by the + /// property. + /// + /// + /// + public ParsingMode Mode { get; set; } + + /// + /// Gets or sets a value that indicates whether the options follow POSIX conventions. + /// + /// + /// if the options follow POSIX conventions; otherwise, + /// . + /// + /// + /// + /// This property is provided as a convenient way to set a number of related properties that + /// together indicate the parser is using POSIX conventions. POSIX conventions in this case + /// means that parsing uses long/short mode, argument names are case sensitive, and argument + /// names and value descriptions use dash-case (e.g. "argument-name"). /// /// - /// This attribute allows you to define your preferred parsing behavior declaratively, on - /// the class that provides the arguments. Apply this attribute to the class to set the - /// properties. + /// Setting this property to is equivalent to setting the + /// property to , the + /// property to , + /// the property to , + /// and the property to . /// /// - /// If you also use the class, any options provided there will - /// override the options set in this attribute. + /// This property will only return if the above properties are the + /// indicated values. It will return for any other combination of + /// values, not just the ones indicated below. /// /// - /// If you wish to use the default options, you do not need to apply this attribute to your - /// class at all. + /// Setting this property to is equivalent to setting the + /// property to , the + /// property to , + /// the property to , + /// and the property to . /// /// - [AttributeUsage(AttributeTargets.Class)] - public class ParseOptionsAttribute : Attribute + /// + public virtual bool IsPosix { - /// - /// Gets or sets a value that indicates the command line argument parsing rules to use. - /// - /// - /// The to use. The default is . - /// - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public ParsingMode Mode { get; set; } + get => Mode == ParsingMode.LongShort && CaseSensitive && ArgumentNameTransform == NameTransform.DashCase && + ValueDescriptionTransform == NameTransform.DashCase; + set + { + if (value) + { + Mode = ParsingMode.LongShort; + CaseSensitive = true; + ArgumentNameTransform = NameTransform.DashCase; + ValueDescriptionTransform = NameTransform.DashCase; + } + else + { + Mode = ParsingMode.Default; + CaseSensitive = false; + ArgumentNameTransform = NameTransform.None; + ValueDescriptionTransform = NameTransform.None; + } + } + } - /// - /// Gets or sets a value that indicates how names are created for arguments that don't have - /// an explicit name. - /// - /// - /// One of the values of the enumeration. The default value is - /// . - /// - /// - /// - /// If an argument doesn't have the - /// property set (or doesn't have an attribute for - /// constructor parameters), the argument name is determined by taking the name of the - /// property, constructor parameter, or method that defines it, and applying the specified - /// transformation. - /// - /// - /// The name transformation will also be applied to the names of the automatically added - /// help and version attributes. - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - public NameTransform ArgumentNameTransform { get; set; } + /// + /// Gets or sets a value that indicates how names are created for arguments that don't have + /// an explicit name. + /// + /// + /// One of the values of the enumeration. The default value is + /// . + /// + /// + /// + /// If an argument doesn't have the + /// property set, the argument name is determined by taking the name of the property or + /// method that defines it, and applying the specified transformation. + /// + /// + /// The name transformation will also be applied to the names of the automatically added + /// help and version attributes. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + public NameTransform ArgumentNameTransform { get; set; } - /// - /// Gets or sets the prefixes that can be used to specify an argument name on the command - /// line. - /// - /// - /// An array of prefixes, or to use the value of - /// . The default value is - /// - /// - /// - /// - /// If the property is , - /// or if the parsing mode is set to - /// elsewhere, this property indicates the short argument name prefixes. Use - /// to set the argument prefix for long names. - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public string[]? ArgumentNamePrefixes { get; set; } + /// + /// Gets or sets the prefixes that can be used to specify an argument name on the command + /// line. + /// + /// + /// An array of prefixes, or to use the value of + /// . The default value is + /// . + /// + /// + /// + /// If the or property + /// is , this property indicates the + /// short argument name prefixes. Use to set the argument + /// prefix for long names. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public string[]? ArgumentNamePrefixes { get; set; } - /// - /// Gets or sets the argument name prefix to use for long argument names. - /// - /// - /// - /// This property is only used if the property is - /// , or if the parsing mode is set to - /// elsewhere. - /// - /// - /// Use the to specify the prefixes for short argument - /// names. - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public string? LongArgumentNamePrefix { get; set; } + /// + /// Gets or sets the argument name prefix to use for long argument names. + /// + /// + /// + /// This property is only used if the property is + /// , or if the parsing mode is set to + /// elsewhere. + /// + /// + /// Use the to specify the prefixes for short argument + /// names. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public string? LongArgumentNamePrefix { get; set; } - /// - /// Gets or sets a value that indicates whether argument names are treated as case - /// sensitive. - /// - /// - /// to indicate that argument names must match case exactly when - /// specified, or to indicate the case does not need to match. - /// The default value is - /// - /// - /// - /// When , the will use - /// for command line argument comparisons; otherwise, - /// it will use . - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public bool CaseSensitive { get; set; } + /// + /// Gets or sets a value that indicates whether argument names are treated as case + /// sensitive. + /// + /// + /// to indicate that argument names must match case exactly when + /// specified, or to indicate the case does not need to match. + /// The default value is + /// + /// + /// + /// When , the will use + /// for command line argument comparisons; otherwise, + /// it will use . Ordinal comparisons are not + /// used for case-sensitive names so that lower and upper case arguments sort together in the usage help. + /// + /// + /// To use a different value than the two mentioned here, use the + /// property. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public bool CaseSensitive { get; set; } - /// - /// Gets or sets a value indicating whether duplicate arguments are allowed. - /// - /// - /// One of the values of the enumeration. The default value is - /// . - /// - /// - /// - /// If set to , supplying a non-multi-value argument more - /// than once will cause an exception. If set to , the - /// last value supplied will be used. - /// - /// - /// If set to , the - /// method, the static method and - /// the class will print a warning to the - /// stream when a duplicate argument is found. If you are - /// not using these methods, is identical to - /// and no warning is displayed. - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public ErrorMode DuplicateArguments { get; set; } + /// + /// Gets or sets a value indicating whether duplicate arguments are allowed. + /// + /// + /// One of the values of the enumeration. The default value is + /// . + /// + /// + /// + /// If set to , supplying a non-multi-value argument more + /// than once will cause an exception. If set to , the + /// last value supplied will be used. + /// + /// + /// If set to , the + /// method, the static + /// method, the generated + /// method, and the class will print + /// a warning to the stream when a + /// duplicate argument is found. If you are not using these methods, + /// is identical to and no warning is + /// displayed. To manually display a warning, use the + /// event. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public ErrorMode DuplicateArguments { get; set; } - /// - /// Gets or sets a value indicating whether the value of arguments may be separated from - /// the name by white space. - /// - /// - /// if white space is allowed to separate an argument name and its - /// value; if only the value from - /// is allowed. The default value is . - /// - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public bool AllowWhiteSpaceValueSeparator { get; set; } = true; + /// + /// Gets or sets a value indicating whether the value of arguments may be separated from + /// the name by white space. + /// + /// + /// if white space is allowed to separate an argument name and its + /// value; if only the values from tne + /// property are allowed. The default value is . + /// + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public bool AllowWhiteSpaceValueSeparator { get; set; } = true; - /// - /// Gets or sets the character used to separate the name and the value of an argument. - /// - /// - /// The character used to separate the name and the value of an argument. The default value is the - /// constant, a colon (:). - /// - /// - /// - /// This character is used to separate the name and the value if both are provided as - /// a single argument to the application, e.g. -sample:value if the default value is used. - /// - /// - /// The character chosen here cannot be used in the name of any parameter. Therefore, - /// it's usually best to choose a non-alphanumeric value such as the colon or equals sign. - /// The character can appear in argument values (e.g. -sample:foo:bar is fine, in which - /// case the value is "foo:bar"). - /// - /// - /// Do not pick a whitespace character as the separator. Doing this only works if the - /// whitespace character is part of the argument, which usually means it needs to be - /// quoted or escaped when invoking your application. Instead, use the - /// property to control whether whitespace - /// is allowed as a separator. - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public char NameValueSeparator { get; set; } = CommandLineParser.DefaultNameValueSeparator; + /// + /// Gets or sets the characters used to separate the name and the value of an argument. + /// + /// + /// The characters used to separate the name and the value of an argument, or + /// to use the default value from the + /// method, which is a colon ':' and an equals sign '='. The default value is . + /// + /// + /// + /// These characters are used to separate the name and the value if both are provided as + /// a single argument to the application, e.g. -sample:value or -sample=value + /// if the default value is used. + /// + /// + /// The character chosen here cannot be used in the name of any parameter. Therefore, + /// it's usually best to choose a non-alphanumeric value such as the colon or equals sign. + /// The character can appear in argument values (e.g. -sample:foo:bar is fine, in which + /// case the value is "foo:bar"). + /// + /// + /// Do not pick a white-space character as the separator. Doing this only works if the + /// whitespace character is part of the argument, which usually means it needs to be + /// quoted or escaped when invoking your application. Instead, use the + /// property to control whether white space + /// is allowed as a separator. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public char[]? NameValueSeparators { get; set; } - /// - /// Gets or sets a value that indicates a help argument will be automatically added. - /// - /// - /// to automatically create a help argument; otherwise, - /// . The default value is . - /// - /// - /// - /// If this property is , the - /// will automatically add an argument with the name "Help". If using , - /// this argument will have the short name "?" and a short alias "h"; otherwise, it - /// will have the aliases "?" and "h". When supplied, this argument will cancel parsing - /// and cause usage help to be printed. - /// - /// - /// If you already have an argument conflicting with the names or aliases above, the - /// automatic help argument will not be created even if this property is - /// . - /// - /// - /// The name, aliases and description can be customized by using a custom . - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - /// - /// - public bool AutoHelpArgument { get; set; } = true; + /// + /// Gets or sets a value that indicates a help argument will be automatically added. + /// + /// + /// to automatically create a help argument; otherwise, + /// . The default value is . + /// + /// + /// + /// If this property is , the + /// will automatically add an argument with the name "Help". If using , + /// this argument will have the short name "?" and a short alias "h"; otherwise, it + /// will have the aliases "?" and "h". When supplied, this argument will cancel parsing + /// and cause usage help to be printed. + /// + /// + /// If you already have an argument conflicting with the names or aliases above, the + /// automatic help argument will not be created even if this property is + /// . + /// + /// + /// The name, aliases and description can be customized by using a custom + /// class. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + /// + /// + public bool AutoHelpArgument { get; set; } = true; - /// - /// Gets or sets a value that indicates a version argument will be automatically added. - /// - /// - /// to automatically create a version argument; otherwise, - /// . The default value is . - /// - /// - /// - /// If this property is , the - /// will automatically add an argument with the name "Version". When supplied, this - /// argument will write version information to the console and cancel parsing, without - /// showing usage help. - /// - /// - /// If you already have an argument named "Version", the automatic version argument - /// will not be created even if this property is . - /// - /// - /// The automatic version argument will never be created for subcommands. - /// - /// - /// The name and description can be customized by using a custom . - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - /// - public bool AutoVersionArgument { get; set; } = true; + /// + /// Gets or sets a value that indicates a version argument will be automatically added. + /// + /// + /// to automatically create a version argument; otherwise, + /// . The default value is . + /// + /// + /// + /// If this property is , the + /// will automatically add an argument with the name "Version". When supplied, this + /// argument will write version information to the console and cancel parsing, without + /// showing usage help. + /// + /// + /// If you already have an argument named "Version", the automatic version argument + /// will not be created even if this property is . + /// + /// + /// The automatic version argument will never be created for subcommands. + /// + /// + /// The name and description can be customized by using a custom + /// class. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + /// + public bool AutoVersionArgument { get; set; } = true; + + /// + /// Gets or sets a value that indicates whether unique prefixes of an argument are automatically + /// used as aliases. + /// + /// + /// to automatically use unique prefixes of an argument as aliases + /// for that argument; otherwise, . The default value is + /// . + /// + /// + /// + /// If this property is , the class + /// will consider any prefix that uniquely identifies an argument by its name or one of its + /// explicit aliases as an alias for that argument. For example, given two arguments "Port" + /// and "Protocol", "Po" and "Por" would be an alias for "Port, and "Pr" an alias for + /// "Protocol" (as well as "Pro", "Prot", "Proto", etc.). "P" would not be an alias because it + /// does not uniquely identify a single argument. + /// + /// + /// When using , this only applies to long names. Explicit + /// aliases set with the take precedence over automatic aliases. + /// Automatic prefix aliases are not shown in the usage help. + /// + /// + /// This behavior is enabled unless explicitly disabled here or using the + /// property. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + public bool AutoPrefixAliases { get; set; } = true; - /// - /// Gets or sets a value that indicates how value descriptions derived from type names - /// are transformed. - /// - /// - /// One of the members of the enumeration. The default value is - /// . - /// - /// - /// - /// This property has no effect on explicit value description specified with the - /// property, the - /// attribute, or the property. - /// - /// - /// This value can be overridden by the - /// property. - /// - /// - /// - public NameTransform ValueDescriptionTransform { get; set; } + /// + /// Gets or sets a value that indicates how value descriptions derived from type names + /// are transformed. + /// + /// + /// One of the members of the enumeration. The default value is + /// . + /// + /// + /// + /// This property has no effect on explicit value description specified with the + /// attribute or the + /// property. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public NameTransform ValueDescriptionTransform { get; set; } - internal IComparer GetStringComparer() + internal StringComparison GetStringComparison() + { + if (CaseSensitive) { - if (CaseSensitive) - { - // Do not use Ordinal for case-sensitive comparisons so that when sorting capitals - // and non-capitals are sorted together. - return StringComparer.InvariantCulture; - } - else - { - return StringComparer.OrdinalIgnoreCase; - } + // Do not use Ordinal for case-sensitive comparisons so that when sorting capitals + // and non-capitals are sorted together. + return StringComparison.InvariantCulture; + } + else + { + return StringComparison.OrdinalIgnoreCase; } } } diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index 61c40a78..18221ad3 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -1,78 +1,118 @@ using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates the result of the last call to the +/// method or one of its overloads. +/// +/// +/// +public readonly struct ParseResult { + private ParseResult(ParseStatus status, CommandLineArgumentException? exception = null, string? argumentName = null, + ReadOnlyMemory remainingArguments = default) + { + Status = status; + LastException = exception; + ArgumentName = argumentName; + RemainingArguments = remainingArguments; + } + /// - /// Indicates the result of the last call to the + /// Gets the status of the last call to the /// method. /// - /// - public readonly struct ParseResult - { - private ParseResult(ParseStatus status, CommandLineArgumentException? exception = null, string? argumentName = null) - { - Status = status; - LastException = exception; - ArgumentName = argumentName; - } + /// + /// One of the values of the enumeration. + /// + public ParseStatus Status { get; } - /// - /// Gets the status of the last call to the - /// method. - /// - /// - /// One of the values of the enumeration. - /// - public ParseStatus Status { get; } + /// + /// Gets the exception that occurred during the last call to the + /// method, if any. + /// + /// + /// The exception, or if parsing was successful or canceled. + /// + public CommandLineArgumentException? LastException { get; } - /// - /// Gets the exception that occurred during the last call to the - /// method, if any. - /// - /// - /// The exception, or if parsing was successful or canceled. - /// - public CommandLineArgumentException? LastException { get; } + /// + /// Gets the name of the argument that caused the error or cancellation. + /// + /// + /// If the property is , + /// the value of the + /// property. If it's , or + /// if + /// was used, the name of the argument that canceled parsing. Otherwise, + /// . + /// + public string? ArgumentName { get; } - /// - /// Gets the name of the argument that caused the error or cancellation. - /// - /// - /// If the property is , the value of - /// the property. If it's - /// , the name of the argument that canceled parsing. - /// Otherwise, . - /// - public string? ArgumentName { get; } + /// + /// Gets any arguments that were not parsed by the if + /// parsing was canceled or an error occurred. + /// + /// + /// A instance with the remaining arguments, or an empty + /// collection if there were no remaining arguments. + /// + /// + /// + /// If parsing succeeded without encountering an argument using , + /// this collection will always be empty. + /// + /// + /// If a exception was thrown, which arguments + /// count as remaining depends on the type of error. For errors that occur during parsing, + /// such as an unknown argument name, value conversion errors, validation errors, + /// duplicate arguments, and others, the remaining arguments will be set to include the + /// argument that threw the exception, and all arguments after it. + /// + /// + /// For errors that occur after parsing is finished, such as validation errors from a + /// validator that uses , or an + /// exception thrown by the target class, this collection will be empty. + /// + /// + public ReadOnlyMemory RemainingArguments { get; } - /// - /// Gets a instance that represents successful parsing. - /// - /// - /// An instance of the structure. - /// - public static ParseResult Success => new(ParseStatus.Success); + /// + /// Gets a instance that represents successful parsing. + /// + /// + /// The name of the argument that canceled parsing using , + /// or if parsing was not canceled. + /// + /// Any remaining arguments that were not parsed. + /// + /// An instance of the structure. + /// + public static ParseResult FromSuccess(string? cancelArgumentName = null, ReadOnlyMemory remainingArguments = default) + => new(ParseStatus.Success, argumentName: cancelArgumentName, remainingArguments: remainingArguments); - /// - /// Creates a instance that represents a parsing error. - /// - /// The exception that occurred during parsing. - /// An instance of the structure. - /// - /// is . - /// - public static ParseResult FromException(CommandLineArgumentException exception) - => new(ParseStatus.Error, exception ?? throw new ArgumentNullException(nameof(exception)), exception.ArgumentName); + /// + /// Creates a instance that represents a parsing error. + /// + /// The exception that occurred during parsing. + /// Any remaining arguments that were not parsed. + /// An instance of the structure. + /// + /// is . + /// + public static ParseResult FromException(CommandLineArgumentException exception, ReadOnlyMemory remainingArguments) + => new(ParseStatus.Error, exception ?? throw new ArgumentNullException(nameof(exception)), exception.ArgumentName, remainingArguments: remainingArguments); - /// - /// Creates a instance that represents canceled parsing. - /// - /// The name of the argument that canceled parsing. - /// An instance of the structure. - /// - /// is . - /// - public static ParseResult FromCanceled(string argumentName) - => new(ParseStatus.Canceled, null, argumentName ?? throw new ArgumentNullException(nameof(argumentName))); - } + /// + /// Creates a instance that represents canceled parsing. + /// + /// The name of the argument that canceled parsing. + /// Any remaining arguments that were not parsed. + /// An instance of the structure. + /// + /// is . + /// + public static ParseResult FromCanceled(string argumentName, ReadOnlyMemory remainingArguments) + => new(ParseStatus.Canceled, null, argumentName ?? throw new ArgumentNullException(nameof(argumentName)), remainingArguments); } diff --git a/src/Ookii.CommandLine/ParseStatus.cs b/src/Ookii.CommandLine/ParseStatus.cs index 44eb0c63..43421548 100644 --- a/src/Ookii.CommandLine/ParseStatus.cs +++ b/src/Ookii.CommandLine/ParseStatus.cs @@ -1,27 +1,29 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates the status of the last call to the +/// method or one of its overloads. +/// +/// +public enum ParseStatus { /// - /// Indicates the status of the last call to the - /// method. + /// The method has not been called yet. /// - /// - public enum ParseStatus - { - /// - /// The method has not been called yet. - /// - None, - /// - /// The operation was successful. - /// - Success, - /// - /// An error occurred while parsing the arguments. - /// - Error, - /// - /// Parsing was canceled by one of the arguments. - /// - Canceled - } + None, + /// + /// The operation successfully parsed all arguments, or was canceled using + /// . Check the + /// property to differentiate between + /// the two. + /// + Success, + /// + /// An error occurred while parsing the arguments. + /// + Error, + /// + /// Parsing was canceled by one of the arguments using . + /// + Canceled } diff --git a/src/Ookii.CommandLine/ParseTypeConverter.cs b/src/Ookii.CommandLine/ParseTypeConverter.cs deleted file mode 100644 index a4f1fa78..00000000 --- a/src/Ookii.CommandLine/ParseTypeConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Reflection; - -namespace Ookii.CommandLine -{ - /// - /// Type converter for types with a public static Parse method. - /// - internal class ParseTypeConverter : TypeConverterBase - { - private readonly MethodInfo _method; - private readonly bool _hasCulture; - - public ParseTypeConverter(MethodInfo method, bool hasCulture) - { - _method = method; - _hasCulture = hasCulture; - } - - protected override object? Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value) - { - var parameters = _hasCulture - ? new object?[] { value, culture } - : new object?[] { value }; - - try - { - return _method.Invoke(null, parameters); - } - catch (Exception ex) - { - // Since we don't know what the method will throw, we'll wrap anything in a - // FormatException. - throw new FormatException(ex.Message, ex); - } - } - } -} diff --git a/src/Ookii.CommandLine/ParsingMode.cs b/src/Ookii.CommandLine/ParsingMode.cs index 205cd9c5..f38dfaae 100644 --- a/src/Ookii.CommandLine/ParsingMode.cs +++ b/src/Ookii.CommandLine/ParsingMode.cs @@ -1,26 +1,25 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates what argument parsing rules should be used to interpret the command line. +/// +/// +/// +/// To set the parsing mode for a , use the +/// property or the property. +/// +/// +/// +public enum ParsingMode { /// - /// Indicates what argument parsing rules should be used to interpret the command line. + /// Use the normal Ookii.CommandLine parsing rules. /// - /// - /// - /// To set the parsing mode for a , use the - /// property or the property. - /// - /// - /// - public enum ParsingMode - { - /// - /// Use the normal Ookii.CommandLine parsing rules. - /// - Default, - /// - /// Allow arguments to have both long and short names, using the - /// to specify a long name, and the regular - /// to specify a short name. - /// - LongShort - } + Default, + /// + /// Allow arguments to have both long and short names, using the + /// to specify a long name, and the regular + /// to specify a short name. + /// + LongShort } diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 81e1566a..5f491e31 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -70,7 +70,7 @@ internal static string ArgumentConversionErrorFormat { } /// - /// Looks up a localized string similar to The name for argument '{0}' contains a colon (:), which is not allowed.. + /// Looks up a localized string similar to The name for argument '{0}' contains one of the name-value separators, which is not allowed.. /// internal static string ArgumentNameContainsSeparatorFormat { get { @@ -159,6 +159,15 @@ internal static string AutomaticVersionName { } } + /// + /// Looks up a localized string similar to The member '{0}' uses CommandLineArgumentAttribute.IsPositional without setting an explicit Position, which is only supported when the GeneratedParserAttribute is used.. + /// + internal static string AutoPositionNotSupportedFormat { + get { + return ResourceManager.GetString("AutoPositionNotSupportedFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The arguments are not valid.. /// @@ -258,15 +267,6 @@ internal static string DuplicateArgumentFormat { } } - /// - /// Looks up a localized string similar to The argument '{0}' has the same position value as the argument '{1}'.. - /// - internal static string DuplicateArgumentPositionFormat { - get { - return ResourceManager.GetString("DuplicateArgumentPositionFormat", resourceCulture); - } - } - /// /// Looks up a localized string similar to Warning: the argument '{0}' was supplied more than once.. /// @@ -312,6 +312,24 @@ internal static string EmptyKeyValueSeparator { } } + /// + /// Looks up a localized string similar to You must specify at least one name-value separator.. + /// + internal static string EmptyNameValueSeparators { + get { + return ResourceManager.GetString("EmptyNameValueSeparators", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The provided ArgumentProvider is not for the type '{0}'.. + /// + internal static string IncorrectProviderTypeFormat { + get { + return ResourceManager.GetString("IncorrectProviderTypeFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The indent must be greater than or equal to zero, and less than the maximum line length.. /// @@ -348,6 +366,15 @@ internal static string InvalidDictionaryValueFormat { } } + /// + /// Looks up a localized string similar to Cannot call the method for this argument.. + /// + internal static string InvalidMethodAccess { + get { + return ResourceManager.GetString("InvalidMethodAccess", resourceCulture); + } + } + /// /// Looks up a localized string similar to The method '{0}' has an unsupported signature.. /// @@ -358,7 +385,7 @@ internal static string InvalidMethodSignatureFormat { } /// - /// Looks up a localized string similar to The command line constructor cannot have non-optional arguments after an optional argument.. + /// Looks up a localized string similar to The command line arguments class cannot have non-optional arguments after an optional argument.. /// internal static string InvalidOptionalArgumentOrder { get { @@ -366,6 +393,15 @@ internal static string InvalidOptionalArgumentOrder { } } + /// + /// Looks up a localized string similar to Cannot get or set the property for this argument.. + /// + internal static string InvalidPropertyAccess { + get { + return ResourceManager.GetString("InvalidPropertyAccess", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid standard stream value.. /// @@ -375,6 +411,24 @@ internal static string InvalidStandardStream { } } + /// + /// Looks up a localized string similar to Invalid value for the StringComparison enumeration.. + /// + internal static string InvalidStringComparison { + get { + return ResourceManager.GetString("InvalidStringComparison", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified TypeConverter cannot converter from a string.. + /// + internal static string InvalidTypeConverter { + get { + return ResourceManager.GetString("InvalidTypeConverter", resourceCulture); + } + } + /// /// Looks up a localized string similar to The 'minimum' and 'maximum' parameters cannot both be null.. /// @@ -421,25 +475,25 @@ internal static string MoreInfoOnErrorFormat { } /// - /// Looks up a localized string similar to The command line arguments type has more than one constructor with the CommandLineConstructorAttribute attribute.. + /// Looks up a localized string similar to The type '{0}' cannot be be parsed from a string using the default conversion rules. Use the ArgumentConverterAttribute to specify a custom ArgumentConverter.. /// - internal static string MultipleMarkedConstructors { + internal static string NoArgumentConverterFormat { get { - return ResourceManager.GetString("MultipleMarkedConstructors", resourceCulture); + return ResourceManager.GetString("NoArgumentConverterFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments type does not have any public constructors.. + /// Looks up a localized string similar to The command does not use custom parsing.. /// - internal static string NoConstructor { + internal static string NoCustomParsing { get { - return ResourceManager.GetString("NoConstructor", resourceCulture); + return ResourceManager.GetString("NoCustomParsing", resourceCulture); } } /// - /// Looks up a localized string similar to A key/value pair must contain "{0}" as a separator.. + /// Looks up a localized string similar to A key-value pair must contain "{0}" as a separator.. /// internal static string NoKeyValuePairSeparatorFormat { get { @@ -456,15 +510,6 @@ internal static string NoLongOrShortName { } } - /// - /// Looks up a localized string similar to The command line arguments type has more than one constructor, none of which has the CommandLineConstructorAttribute attribute.. - /// - internal static string NoMarkedConstructor { - get { - return ResourceManager.GetString("NoMarkedConstructor", resourceCulture); - } - } - /// /// Looks up a localized string similar to Cannot create a parser for a command with custom parsing.. /// @@ -475,25 +520,25 @@ internal static string NoParserForCustomParsingCommand { } /// - /// Looks up a localized string similar to No type converter that can convert to and from a string exists for type '{0}'. Use the TypeConverterAttribute to specify a custom TypeConverter.. + /// Looks up a localized string similar to The argument '{0}' cannot be null.. /// - internal static string NoTypeConverterFormat { + internal static string NullArgumentValueFormat { get { - return ResourceManager.GetString("NoTypeConverterFormat", resourceCulture); + return ResourceManager.GetString("NullArgumentValueFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The argument '{0}' cannot be null.. + /// Looks up a localized string similar to A read-only property for a multi-value or dictionary argument returned null.. /// - internal static string NullArgumentValueFormat { + internal static string NullPropertyValue { get { - return ResourceManager.GetString("NullArgumentValueFormat", resourceCulture); + return ResourceManager.GetString("NullPropertyValue", resourceCulture); } } /// - /// Looks up a localized string similar to The property defining the argument '{0}' doesn't have a public set accessor.. + /// Looks up a localized string similar to The property defining the argument '{0}' does not have a public set accessor.. /// internal static string PropertyIsReadOnlyFormat { get { @@ -529,20 +574,20 @@ internal static string TooManyArguments { } /// - /// Looks up a localized string similar to Could not convert type '{0}' to '{1}' for argument '{2}'.. + /// Looks up a localized string similar to The type '{0}' does not implement the ICommand interface or does not have the CommandAttribute attribute.. /// - internal static string TypeConversionErrorFormat { + internal static string TypeIsNotCommandFormat { get { - return ResourceManager.GetString("TypeConversionErrorFormat", resourceCulture); + return ResourceManager.GetString("TypeIsNotCommandFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The type '{0}' does not implement the ICommand interface or does not have the CommandAttribute attribute.. + /// Looks up a localized string similar to The type '{0}' is not an enumeration type.. /// - internal static string TypeIsNotCommandFormat { + internal static string TypeIsNotEnumFormat { get { - return ResourceManager.GetString("TypeIsNotCommandFormat", resourceCulture); + return ResourceManager.GetString("TypeIsNotEnumFormat", resourceCulture); } } @@ -610,7 +655,7 @@ internal static string ValidateCountBothFormat { } /// - /// Looks up a localized string similar to The argument '{0}' must have at most {1} items.. + /// Looks up a localized string similar to The argument '{0}' must have at most {1} item(s).. /// internal static string ValidateCountMaxFormat { get { @@ -619,7 +664,7 @@ internal static string ValidateCountMaxFormat { } /// - /// Looks up a localized string similar to The argument '{0}' must have at least {1} items.. + /// Looks up a localized string similar to The argument '{0}' must have at least {1} item(s).. /// internal static string ValidateCountMinFormat { get { @@ -637,7 +682,7 @@ internal static string ValidateCountUsageHelpBothFormat { } /// - /// Looks up a localized string similar to Must have at most {0} items.. + /// Looks up a localized string similar to Must have at most {0} item(s).. /// internal static string ValidateCountUsageHelpMaxFormat { get { @@ -646,7 +691,7 @@ internal static string ValidateCountUsageHelpMaxFormat { } /// - /// Looks up a localized string similar to Must have at least {0} items.. + /// Looks up a localized string similar to Must have at least {0} item(s).. /// internal static string ValidateCountUsageHelpMinFormat { get { @@ -826,7 +871,7 @@ internal static string ValidateStringLengthBothFormat { } /// - /// Looks up a localized string similar to The argument '{0}' must be at most {1} characters.. + /// Looks up a localized string similar to The argument '{0}' must be at most {1} character(s).. /// internal static string ValidateStringLengthMaxFormat { get { @@ -835,7 +880,7 @@ internal static string ValidateStringLengthMaxFormat { } /// - /// Looks up a localized string similar to The argument '{0}' must be at least {1} characters.. + /// Looks up a localized string similar to The argument '{0}' must be at least {1} character(s).. /// internal static string ValidateStringLengthMinFormat { get { @@ -853,7 +898,7 @@ internal static string ValidateStringLengthUsageHelpBothFormat { } /// - /// Looks up a localized string similar to Must be at most {0} characters.. + /// Looks up a localized string similar to Must be at most {0} character(s).. /// internal static string ValidateStringLengthUsageHelpMaxFormat { get { @@ -862,7 +907,7 @@ internal static string ValidateStringLengthUsageHelpMaxFormat { } /// - /// Looks up a localized string similar to Must be at least {0} characters.. + /// Looks up a localized string similar to Must be at least {0} character(s).. /// internal static string ValidateStringLengthUsageHelpMinFormat { get { diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index a7005e97..ac855b2d 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -138,9 +138,6 @@ The argument '{0}' was supplied more than once. - - The argument '{0}' has the same position value as the argument '{1}'. - You must specify at least one argument name prefix. @@ -157,7 +154,7 @@ Multi-dimensional arrays are not supported for command line arguments. - The command line constructor cannot have non-optional arguments after an optional argument. + The command line arguments class cannot have non-optional arguments after an optional argument. The specified property or method is not a command line argument. @@ -168,17 +165,8 @@ No value was supplied for the argument '{0}'. - - The command line arguments type has more than one constructor with the CommandLineConstructorAttribute attribute. - - The name for argument '{0}' contains a colon (:), which is not allowed. - - - The command line arguments type does not have any public constructors. - - - The command line arguments type has more than one constructor, none of which has the CommandLineConstructorAttribute attribute. + The name for argument '{0}' contains one of the name-value separators, which is not allowed. Too many arguments were supplied. @@ -193,7 +181,7 @@ The value must be zero or larger. - The property defining the argument '{0}' doesn't have a public set accessor. + The property defining the argument '{0}' does not have a public set accessor. The type must be a generic type definition. @@ -202,10 +190,10 @@ The value '{1}' provided for argument '{0}' was invalid: {2} - A key/value pair must contain "{0}" as a separator. + A key-value pair must contain "{0}" as a separator. - - No type converter that can convert to and from a string exists for type '{0}'. Use the TypeConverterAttribute to specify a custom TypeConverter. + + The type '{0}' cannot be be parsed from a string using the default conversion rules. Use the ArgumentConverterAttribute to specify a custom ArgumentConverter. An error occurred creating an instance of the arguments type: {0} @@ -267,17 +255,14 @@ The 'minimum' and 'maximum' parameters cannot both be null. - - Could not convert type '{0}' to '{1}' for argument '{2}'. - The argument '{0}' must have between {1} and {2} items. - The argument '{0}' must have at most {1} items. + The argument '{0}' must have at most {1} item(s). - The argument '{0}' must have at least {1} items. + The argument '{0}' must have at least {1} item(s). The argument '{0}' must not be empty. @@ -295,10 +280,10 @@ The argument '{0}' must be between {1} and {2} characters. - The argument '{0}' must be at most {1} characters. + The argument '{0}' must be at most {1} character(s). - The argument '{0}' must be at least {1} characters. + The argument '{0}' must be at least {1} character(s). The argument '{0}' must not be empty or contain only white-space characters. @@ -334,10 +319,10 @@ Must have between {0} and {1} items. - Must have at most {0} items. + Must have at most {0} item(s). - Must have at least {0} items. + Must have at least {0} item(s). Must not be empty. @@ -355,10 +340,10 @@ Must be between {0} and {1} characters. - Must be at most {0} characters. + Must be at most {0} character(s). - Must be at least {0} characters. + Must be at least {0} character(s). A {0} refers to an unknown argument '{1}'. @@ -393,4 +378,34 @@ An async write operation is already in progress. + + The provided ArgumentProvider is not for the type '{0}'. + + + The command does not use custom parsing. + + + Invalid value for the StringComparison enumeration. + + + The specified TypeConverter cannot converter from a string. + + + You must specify at least one name-value separator. + + + The member '{0}' uses CommandLineArgumentAttribute.IsPositional without setting an explicit Position, which is only supported when the GeneratedParserAttribute is used. + + + Cannot call the method for this argument. + + + Cannot get or set the property for this argument. + + + A read-only property for a multi-value or dictionary argument returned null. + + + The type '{0}' is not an enumeration type. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/RingBuffer.Async.cs b/src/Ookii.CommandLine/RingBuffer.Async.cs index 22dfac21..dc9ca7b6 100644 --- a/src/Ookii.CommandLine/RingBuffer.Async.cs +++ b/src/Ookii.CommandLine/RingBuffer.Async.cs @@ -3,38 +3,47 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; using System.Threading.Tasks; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +internal partial class RingBuffer { - internal partial class RingBuffer + public async Task WriteToAsync(TextWriter writer, int length, CancellationToken cancellationToken) { - public async Task WriteToAsync(TextWriter writer, int length) + if (length > Size) { - if (length > Size) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } + throw new ArgumentOutOfRangeException(nameof(length)); + } - var remaining = _buffer.Length - _bufferStart; - if (remaining < length) - { - await writer.WriteAsync(_buffer, _bufferStart, remaining); - remaining = length - remaining; - await writer.WriteAsync(_buffer, 0, remaining); - _bufferStart = remaining; - } - else - { - await writer.WriteAsync(_buffer, _bufferStart, length); - _bufferStart += length; - Debug.Assert(_bufferStart <= _buffer.Length); - } + var remaining = _buffer.Length - _bufferStart; + if (remaining < length) + { + await WriteAsyncHelper(writer, _buffer, _bufferStart, remaining, cancellationToken); + remaining = length - remaining; + await WriteAsyncHelper(writer, _buffer, 0, remaining, cancellationToken); + _bufferStart = remaining; + } + else + { + await WriteAsyncHelper(writer, _buffer, _bufferStart, length, cancellationToken); + _bufferStart += length; + Debug.Assert(_bufferStart <= _buffer.Length); + } - if (_bufferEnd != null && _bufferStart == _bufferEnd.Value) - { - _bufferEnd = null; - } + if (_bufferEnd != null && _bufferStart == _bufferEnd.Value) + { + _bufferEnd = null; } } + + private static async Task WriteAsyncHelper(TextWriter writer, char[] buffer, int index, int length, CancellationToken cancellationToken) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await writer.WriteAsync(buffer.AsMemory(index, length), cancellationToken); +#else + await writer.WriteAsync(buffer, index, length); +#endif + } } diff --git a/src/Ookii.CommandLine/RingBuffer.cs b/src/Ookii.CommandLine/RingBuffer.cs index 13e02b14..d9ff7928 100644 --- a/src/Ookii.CommandLine/RingBuffer.cs +++ b/src/Ookii.CommandLine/RingBuffer.cs @@ -2,197 +2,198 @@ using System.Diagnostics; using System.IO; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +internal partial class RingBuffer { - internal partial class RingBuffer + private char[] _buffer; + private int _bufferStart; + private int? _bufferEnd; + + public RingBuffer(int size) { - private char[] _buffer; - private int _bufferStart; - private int? _bufferEnd; + _buffer = new char[size]; + _bufferStart = 0; + _bufferEnd = null; + } - public RingBuffer(int size) + public int Size + { + get { - _buffer = new char[size]; - _bufferStart = 0; - _bufferEnd = null; + if (_bufferEnd == null) + { + return 0; + } + + return _bufferEnd.Value > _bufferStart + ? _bufferEnd.Value - _bufferStart + : Capacity - _bufferStart + _bufferEnd.Value; } + } + public int Capacity => _buffer.Length; - public int Size + public char this[int index] + { + get { - get + index += _bufferStart; + if (index >= _buffer.Length) { - if (_bufferEnd == null) - { - return 0; - } - - return _bufferEnd.Value > _bufferStart - ? _bufferEnd.Value - _bufferStart - : Capacity - _bufferStart + _bufferEnd.Value; + index -= _buffer.Length; } - } - public int Capacity => _buffer.Length; - public char this[int index] - { - get + if (index < _bufferStart && index >= _bufferEnd) { - index += _bufferStart; - if (index >= _buffer.Length) - { - index -= _buffer.Length; - } - - if (index < _bufferStart && index >= _bufferEnd) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return _buffer[index]; + throw new ArgumentOutOfRangeException(nameof(index)); } + + return _buffer[index]; } + } - public void CopyFrom(ReadOnlySpan span) + public void CopyFrom(ReadOnlySpan span) + { + int size = Size; + if (span.Length > Capacity - size) { - int size = Size; - if (span.Length > Capacity - size) - { - Resize(size + span.Length); - } + Resize(size + span.Length); + } - int contentEnd = _bufferEnd ?? _bufferStart; - var remaining = _buffer.Length - contentEnd; - if (remaining < span.Length) - { - var (first, second) = span.Split(remaining); - first.CopyTo(_buffer, contentEnd); - second.CopyTo(_buffer, 0); - _bufferEnd = second.Length; - } - else - { - span.CopyTo(_buffer, contentEnd); - _bufferEnd = contentEnd + span.Length; - Debug.Assert(_bufferEnd <= _buffer.Length); - } + int contentEnd = _bufferEnd ?? _bufferStart; + var remaining = _buffer.Length - contentEnd; + if (remaining < span.Length) + { + var (first, second) = span.Split(remaining); + first.CopyTo(_buffer, contentEnd); + second.CopyTo(_buffer, 0); + _bufferEnd = second.Length; } + else + { + span.CopyTo(_buffer, contentEnd); + _bufferEnd = contentEnd + span.Length; + Debug.Assert(_bufferEnd <= _buffer.Length); + } + } - public partial void WriteTo(TextWriter writer, int length); + public partial void WriteTo(TextWriter writer, int length); - public void Discard(int length) + public void Discard(int length) + { + var remaining = _buffer.Length - _bufferStart; + if (remaining < length) { - var remaining = _buffer.Length - _bufferStart; - if (remaining < length) - { - _bufferStart = length - remaining; - } - else - { - _bufferStart += length; - Debug.Assert(_bufferStart <= _buffer.Length); - } + _bufferStart = length - remaining; + } + else + { + _bufferStart += length; + Debug.Assert(_bufferStart <= _buffer.Length); + } - if (_bufferEnd != null && _bufferStart == _bufferEnd.Value) - { - _bufferEnd = null; - } + if (_bufferEnd != null && _bufferStart == _bufferEnd.Value) + { + _bufferEnd = null; } + } - public StringSpanTuple GetContents(int offset) + public StringSpanTuple GetContents(int offset) + { + if (offset < 0 || offset > Size) { - if (offset < 0 || offset > Size) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } + throw new ArgumentOutOfRangeException(nameof(offset)); + } - if (_bufferEnd == null) - { - return default; - } + if (_bufferEnd == null) + { + return default; + } - int start = _bufferStart + offset; - if (start >= _buffer.Length) - { - start -= _buffer.Length; - } + int start = _bufferStart + offset; + if (start >= _buffer.Length) + { + start -= _buffer.Length; + } - if (start > _bufferEnd.Value) - { - return new(new ReadOnlySpan(_buffer, _bufferStart, _buffer.Length - _bufferStart), new ReadOnlySpan(_buffer, 0, _bufferEnd.Value)); - } + if (start > _bufferEnd.Value) + { + return new(new ReadOnlySpan(_buffer, _bufferStart, _buffer.Length - _bufferStart), new ReadOnlySpan(_buffer, 0, _bufferEnd.Value)); + } + + return new(new ReadOnlySpan(_buffer, start, _bufferEnd.Value - start), default); + } - return new(new ReadOnlySpan(_buffer, start, _bufferEnd.Value - start), default); + public void Peek(TextWriter writer, int offset, int length) + { + var (first, second) = GetContents(offset); + first.Slice(0, Math.Min(length, first.Length)).WriteTo(writer); + if (length > first.Length) + { + second.Slice(0, Math.Min(length - first.Length, second.Length)).WriteTo(writer); } + } - public void Peek(TextWriter writer, int offset, int length) + public int BreakLine(int offset, int length) + { + int size = Size; + if (offset < 0 || offset > size) { - var (first, second) = GetContents(offset); - first.Slice(0, Math.Min(length, first.Length)).WriteTo(writer); - if (length > first.Length) - { - second.Slice(0, Math.Min(length - first.Length, second.Length)).WriteTo(writer); - } + throw new ArgumentOutOfRangeException(nameof(offset)); } - public int BreakLine(int offset, int length) + if (offset + length > size) { - int size = Size; - if (offset < 0 || offset > size) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } + throw new ArgumentOutOfRangeException(nameof(length)); + } - if (offset + length > size) + for (int i = offset + length - 1; i >= offset; i--) + { + if (char.IsWhiteSpace(this[i])) { - throw new ArgumentOutOfRangeException(nameof(length)); + return i; } + } - for (int i = offset + length - 1; i >= offset; i--) - { - if (char.IsWhiteSpace(this[i])) - { - return i; - } - } + return -1; + } - return -1; - } + private void Resize(int capacityNeeded) + { + var newCapacity = 2 * _buffer.Length; - private void Resize(int capacityNeeded) + // Check for overflow + if (newCapacity < 0) { - var newCapacity = 2 * _buffer.Length; + newCapacity = int.MaxValue; + } - // Check for overflow - if (newCapacity < 0) - { - newCapacity = int.MaxValue; - } + if (capacityNeeded > newCapacity) + { + newCapacity = capacityNeeded; + } - if (capacityNeeded > newCapacity) + var newBuffer = new char[newCapacity]; + int size = Size; + if (_bufferEnd != null) + { + if (_bufferStart >= _bufferEnd) { - newCapacity = capacityNeeded; + int length = _buffer.Length - _bufferStart; + Array.Copy(_buffer, _bufferStart, newBuffer, 0, length); + Array.Copy(_buffer, 0, newBuffer, length, _bufferEnd.Value); } - - var newBuffer = new char[newCapacity]; - int size = Size; - if (_bufferEnd != null) + else { - if (_bufferStart >= _bufferEnd) - { - int length = _buffer.Length - _bufferStart; - Array.Copy(_buffer, _bufferStart, newBuffer, 0, length); - Array.Copy(_buffer, 0, newBuffer, length, _bufferEnd.Value); - } - else - { - Array.Copy(_buffer, _bufferStart, newBuffer, 0, _bufferEnd.Value - _bufferStart); - } - - _bufferEnd = size; + Array.Copy(_buffer, _bufferStart, newBuffer, 0, _bufferEnd.Value - _bufferStart); } - _bufferStart = 0; - _buffer = newBuffer; + _bufferEnd = size; } + + _bufferStart = 0; + _buffer = newBuffer; } + + private static partial void WriteHelper(TextWriter writer, char[] buffer, int index, int length); } diff --git a/src/Ookii.CommandLine/ShortAliasAttribute.cs b/src/Ookii.CommandLine/ShortAliasAttribute.cs index 900a47a6..23ba9383 100644 --- a/src/Ookii.CommandLine/ShortAliasAttribute.cs +++ b/src/Ookii.CommandLine/ShortAliasAttribute.cs @@ -1,55 +1,54 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Defines an alternative short name for a command line argument. +/// +/// +/// +/// To specify multiple aliases, apply this attribute multiple times. +/// +/// +/// This attribute specifies short name aliases used with . +/// It is ignored if the property is not +/// , or if the argument doesn't have a +/// primary . +/// +/// +/// The short aliases for a command line argument can be used instead of the regular short +/// name to specify the parameter on the command line. +/// +/// +/// All short argument names and short aliases defined by a single arguments type must be +/// unique. +/// +/// +/// By default, the command line usage help generated by +/// includes the aliases. Set the +/// property to to exclude them. +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] +public sealed class ShortAliasAttribute : Attribute { + private readonly char _alias; + /// - /// Defines an alternative short name for a command line argument. + /// Initializes a new instance of the class. /// - /// - /// - /// To specify multiple aliases, apply this attribute multiple times. - /// - /// - /// This attribute specifies short name aliases used with - /// mode. It is ignored if the property is not - /// , or if the argument doesn't have a primary - /// . - /// - /// - /// The short aliases for a command line argument can be used instead of the regular short - /// name to specify the parameter on the command line. - /// - /// - /// All short argument names and short aliases defined by a single arguments type must be - /// unique. - /// - /// - /// By default, the command line usage help generated by - /// includes the aliases. Set the - /// property to to exclude them. - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] - public sealed class ShortAliasAttribute : Attribute + /// The alternative short name for the command line argument. + public ShortAliasAttribute(char alias) { - private readonly char _alias; - - /// - /// Initializes a new instance of the class. - /// - /// The alternative short name for the command line argument. - public ShortAliasAttribute(char alias) - { - _alias = alias; - } - - /// - /// Gets the alternative short name for the command line argument. - /// - /// - /// The alternative short name for the command line argument. - /// - public char Alias => _alias; + _alias = alias; } + + /// + /// Gets the alternative short name for the command line argument. + /// + /// + /// The alternative short name for the command line argument. + /// + public char Alias => _alias; } diff --git a/src/Ookii.CommandLine/StringComparisonExtensions.cs b/src/Ookii.CommandLine/StringComparisonExtensions.cs new file mode 100644 index 00000000..3bbf25d4 --- /dev/null +++ b/src/Ookii.CommandLine/StringComparisonExtensions.cs @@ -0,0 +1,27 @@ +using System; + +namespace Ookii.CommandLine; + +internal static class StringComparisonExtensions +{ + public static StringComparer GetComparer(this StringComparison comparison) + { +#if NETSTANDARD2_0 + return comparison switch + { + StringComparison.CurrentCulture => StringComparer.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, + StringComparison.Ordinal => StringComparer.Ordinal, + StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, + _ => throw new ArgumentException(Properties.Resources.InvalidStringComparison, nameof(comparison)) + }; +#else + return StringComparer.FromComparison(comparison); +#endif + } + + public static bool IsCaseSensitive(this StringComparison comparison) + => comparison is StringComparison.Ordinal or StringComparison.InvariantCulture or StringComparison.CurrentCulture; +} diff --git a/src/Ookii.CommandLine/StringExtensions.cs b/src/Ookii.CommandLine/StringExtensions.cs index b2cc8ebe..117dd160 100644 --- a/src/Ookii.CommandLine/StringExtensions.cs +++ b/src/Ookii.CommandLine/StringExtensions.cs @@ -1,29 +1,50 @@ -namespace Ookii.CommandLine +using System; + +namespace Ookii.CommandLine; + +internal static class StringExtensions { - internal static class StringExtensions + public static (ReadOnlyMemory, ReadOnlyMemory?) SplitOnce(this ReadOnlyMemory value, char separator) { - public static (string, string?) SplitOnce(this string value, char separator, int start = 0) - { - var index = value.IndexOf(separator, start); - return value.SplitAt(index, start, 1); - } + var index = value.Span.IndexOf(separator); + return value.SplitAt(index, 1); + } + + public static (ReadOnlyMemory, ReadOnlyMemory?) SplitFirstOfAny(this ReadOnlyMemory value, ReadOnlySpan separators) + { + var index = value.Span.IndexOfAny(separators); + return value.SplitAt(index, 1); + } - public static (string, string?) SplitOnce(this string value, string separator, int start = 0) + public static StringSpanTuple SplitOnce(this ReadOnlySpan value, ReadOnlySpan separator, out bool hasSeparator) + { + var index = value.IndexOf(separator); + return value.SplitAt(index, separator.Length, out hasSeparator); + } + + private static (ReadOnlyMemory, ReadOnlyMemory?) SplitAt(this ReadOnlyMemory value, int index, int skip) + { + if (index < 0) { - var index = value.IndexOf(separator); - return value.SplitAt(index, start, separator.Length); + return (value, null); } - private static (string, string?) SplitAt(this string value, int index, int start, int skip) + var before = value.Slice(0, index); + var after = value.Slice(index + skip); + return (before, after); + } + + private static StringSpanTuple SplitAt(this ReadOnlySpan value, int index, int skip, out bool hasSeparator) + { + if (index < 0) { - if (index < 0) - { - return (value.Substring(start), null); - } - - var before = value.Substring(start, index - start); - var after = value.Substring(index + skip); - return (before, after); + hasSeparator = false; + return new(value, default); } + + var before = value.Slice(0, index); + var after = value.Slice(index + skip); + hasSeparator = true; + return new(before, after); } } diff --git a/src/Ookii.CommandLine/StringSegmentType.cs b/src/Ookii.CommandLine/StringSegmentType.cs index 717562c1..8beb853e 100644 --- a/src/Ookii.CommandLine/StringSegmentType.cs +++ b/src/Ookii.CommandLine/StringSegmentType.cs @@ -1,16 +1,15 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +enum StringSegmentType { - enum StringSegmentType - { - Text, - LineBreak, - Formatting, - PartialLineBreak, - // Must be the last group of values in the enum - PartialFormattingUnknown, - PartialFormattingSimple, - PartialFormattingCsi, - PartialFormattingOsc, - PartialFormattingOscWithEscape, - } + Text, + LineBreak, + Formatting, + PartialLineBreak, + // Must be the last group of values in the enum + PartialFormattingUnknown, + PartialFormattingSimple, + PartialFormattingCsi, + PartialFormattingOsc, + PartialFormattingOscWithEscape, } diff --git a/src/Ookii.CommandLine/StringSpan.Async.cs b/src/Ookii.CommandLine/StringSpan.Async.cs index 8dd324a2..227e5946 100644 --- a/src/Ookii.CommandLine/StringSpan.Async.cs +++ b/src/Ookii.CommandLine/StringSpan.Async.cs @@ -3,75 +3,75 @@ using Ookii.CommandLine.Terminal; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + + +internal static partial class StringSpanExtensions { - internal static partial class StringSpanExtensions + public static async Task WriteToAsync(this ReadOnlyMemory self, TextWriter writer, CancellationToken cancellationToken) { - - public static async Task WriteToAsync(this ReadOnlyMemory self, TextWriter writer) - { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await writer.WriteAsync(self); + await writer.WriteAsync(self, cancellationToken); #else - await writer.WriteAsync(self.ToString()); + await writer.WriteAsync(self.ToString()); #endif - } + } - public static async Task SplitAsync(this ReadOnlyMemory self, bool newLinesOnly, AsyncCallback callback) + public static async Task SplitAsync(this ReadOnlyMemory self, bool newLinesOnly, AsyncCallback callback) + { + var separators = newLinesOnly ? _newLineSeparators : _segmentSeparators; + var remaining = self; + while (remaining.Span.Length > 0) { - var separators = newLinesOnly ? _newLineSeparators : _segmentSeparators; - var remaining = self; - while (remaining.Span.Length > 0) + var separatorIndex = remaining.Span.IndexOfAny(separators); + if (separatorIndex < 0) + { + await callback(StringSegmentType.Text, remaining); + break; + } + + if (separatorIndex > 0) + { + await callback(StringSegmentType.Text, remaining.Slice(0, separatorIndex)); + remaining = remaining.Slice(separatorIndex); + } + + if (remaining.Span[0] == VirtualTerminal.Escape) { - var separatorIndex = remaining.Span.IndexOfAny(separators); - if (separatorIndex < 0) + // This is a VT sequence. + // Find the end of the sequence. + StringSegmentType type = StringSegmentType.PartialFormattingUnknown; + var end = VirtualTerminal.FindSequenceEnd(remaining.Slice(1).Span, ref type); + if (end == -1) { - await callback(StringSegmentType.Text, remaining); + // No end? Should come in a following write. + await callback(type, remaining); break; } - if (separatorIndex > 0) - { - await callback(StringSegmentType.Text, remaining.Slice(0, separatorIndex)); - remaining = remaining.Slice(separatorIndex); - } + // Add one for the escape character, and one to skip past the end. + end += 2; + await callback(StringSegmentType.Formatting, remaining.Slice(0, end)); + remaining = remaining.Slice(end); + } + else + { + ReadOnlyMemory lineBreak; + (lineBreak, remaining) = remaining.SkipLineBreak(); - if (remaining.Span[0] == VirtualTerminal.Escape) + if (remaining.Span.Length == 0 && lineBreak.Span.Length == 1 && lineBreak.Span[0] == '\r') { - // This is a VT sequence. - // Find the end of the sequence. - StringSegmentType type = StringSegmentType.PartialFormattingUnknown; - var end = VirtualTerminal.FindSequenceEnd(remaining.Slice(1).Span, ref type); - if (end == -1) - { - // No end? Should come in a following write. - await callback(type, remaining); - break; - } - - // Add one for the escape character, and one to skip past the end. - end += 2; - await callback(StringSegmentType.Formatting, remaining.Slice(0, end)); - remaining = remaining.Slice(end); + // This could be the start of a Windows-style break, the remainder of + // which could follow in the next span. + await callback(StringSegmentType.PartialLineBreak, lineBreak); + break; } - else - { - ReadOnlyMemory lineBreak; - (lineBreak, remaining) = remaining.SkipLineBreak(); - if (remaining.Span.Length == 0 && lineBreak.Span.Length == 1 && lineBreak.Span[0] == '\r') - { - // This could be the start of a Windows-style break, the remainder of - // which could follow in the next span. - await callback(StringSegmentType.PartialLineBreak, lineBreak); - break; - } - - await callback(StringSegmentType.LineBreak, lineBreak); - } + await callback(StringSegmentType.LineBreak, lineBreak); } } } diff --git a/src/Ookii.CommandLine/StringSpanExtensions.cs b/src/Ookii.CommandLine/StringSpanExtensions.cs index 676fc2fe..0bbd211e 100644 --- a/src/Ookii.CommandLine/StringSpanExtensions.cs +++ b/src/Ookii.CommandLine/StringSpanExtensions.cs @@ -4,110 +4,124 @@ using System.IO; using System.Threading.Tasks; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +// These methods are declared as extension methods so they can be used with ReadOnlySpan on +// .Net Standard 2.0 and with ReadOnlySpan on .Net Standard 2.1. +internal static partial class StringSpanExtensions { - // These methods are declared as extension methods so they can be used with ReadOnlySpan on - // .Net Standard 2.0 and with ReadOnlySpan on .Net Standard 2.1. - internal static partial class StringSpanExtensions + public delegate void Callback(StringSegmentType type, ReadOnlySpan span); + public delegate Task AsyncCallback(StringSegmentType type, ReadOnlyMemory span); + public delegate bool SplitCallback(ReadOnlySpan span); + + private static readonly char[] _segmentSeparators = { '\r', '\n', VirtualTerminal.Escape }; + private static readonly char[] _newLineSeparators = { '\r', '\n' }; + + public static partial void Split(this ReadOnlySpan self, bool newLinesOnly, Callback callback); + + public static StringSpanTuple SkipLineBreak(this ReadOnlySpan self) { - public delegate void Callback(StringSegmentType type, ReadOnlySpan span); - public delegate Task AsyncCallback(StringSegmentType type, ReadOnlyMemory span); + Debug.Assert(self[0] is '\r' or '\n'); + var split = self[0] == '\r' && self.Length > 1 && self[1] == '\n' + ? 2 + : 1; - private static readonly char[] _segmentSeparators = { '\r', '\n', VirtualTerminal.Escape }; - private static readonly char[] _newLineSeparators = { '\r', '\n' }; + return self.Split(split); + } - public static partial void Split(this ReadOnlySpan self, bool newLinesOnly, Callback callback); + public static StringSpanTuple Split(this ReadOnlySpan self, int index) + => new(self.Slice(0, index), self.Slice(index)); - public static StringSpanTuple SkipLineBreak(this ReadOnlySpan self) + // On .Net 6 StringSpanTuple is a ref struct so it can't be used with Nullable, so use + // an out param instead. + public static bool BreakLine(this ReadOnlySpan self, int startIndex, BreakLineMode mode, out StringSpanTuple splits) + { + if (BreakLine(self, startIndex, mode) is var (end, start)) { - Debug.Assert(self[0] is '\r' or '\n'); - var split = self[0] == '\r' && self.Length > 1 && self[1] == '\n' - ? 2 - : 1; - - return self.Split(split); + splits = new(self.Slice(0, end), self.Slice(start)); + return true; } - public static StringSpanTuple Split(this ReadOnlySpan self, int index) - => new(self.Slice(0, index), self.Slice(index)); + splits = default; + return false; + } - // On .Net 6 StringSpanTuple is a ref struct so it can't be used with Nullable, so use - // an out param instead. - public static bool BreakLine(this ReadOnlySpan self, int startIndex, BreakLineMode mode, out StringSpanTuple splits) - { - if (BreakLine(self, startIndex, mode) is var (end, start)) - { - splits = new(self.Slice(0, end), self.Slice(start)); - return true; - } + public static (ReadOnlyMemory, ReadOnlyMemory) SkipLineBreak(this ReadOnlyMemory self) + { + Debug.Assert(self.Span[0] is '\r' or '\n'); + var split = self.Span[0] == '\r' && self.Span.Length > 1 && self.Span[1] == '\n' + ? 2 + : 1; - splits = default; - return false; - } + return self.Split(split); + } - public static (ReadOnlyMemory, ReadOnlyMemory) SkipLineBreak(this ReadOnlyMemory self) - { - Debug.Assert(self.Span[0] is '\r' or '\n'); - var split = self.Span[0] == '\r' && self.Span.Length > 1 && self.Span[1] == '\n' - ? 2 - : 1; + public static (ReadOnlyMemory, ReadOnlyMemory) Split(this ReadOnlyMemory self, int index) + => new(self.Slice(0, index), self.Slice(index)); - return self.Split(split); + public static bool BreakLine(this ReadOnlyMemory self, int startIndex, BreakLineMode mode, out (ReadOnlyMemory, ReadOnlyMemory) splits) + { + if (BreakLine(self.Span, startIndex, mode) is var (end, start)) + { + splits = new(self.Slice(0, end), self.Slice(start)); + return true; } - public static (ReadOnlyMemory, ReadOnlyMemory) Split(this ReadOnlyMemory self, int index) - => new(self.Slice(0, index), self.Slice(index)); + splits = default; + return false; + } + + public static void CopyTo(this ReadOnlySpan self, char[] destination, int start) + { + self.CopyTo(destination.AsSpan(start)); + } - public static bool BreakLine(this ReadOnlyMemory self, int startIndex, BreakLineMode mode, out (ReadOnlyMemory, ReadOnlyMemory) splits) + public static void Split(this ReadOnlySpan self, ReadOnlySpan separator, SplitCallback callback) + { + while (!self.IsEmpty) { - if (BreakLine(self.Span, startIndex, mode) is var (end, start)) + var (first, remaining) = self.SplitOnce(separator, out bool _); + if (!callback(first)) { - splits = new(self.Slice(0, end), self.Slice(start)); - return true; + break; } - splits = default; - return false; - } - - public static void CopyTo(this ReadOnlySpan self, char[] destination, int start) - { - self.CopyTo(destination.AsSpan(start)); + self = remaining; } + } - public static partial void WriteTo(this ReadOnlySpan self, TextWriter writer); + public static partial void WriteTo(this ReadOnlySpan self, TextWriter writer); - private static (int, int)? BreakLine(ReadOnlySpan span, int startIndex, BreakLineMode mode) + private static (int, int)? BreakLine(ReadOnlySpan span, int startIndex, BreakLineMode mode) + { + switch (mode) { - switch (mode) - { - case BreakLineMode.Force: - return (startIndex, startIndex); + case BreakLineMode.Force: + return (startIndex, startIndex); - case BreakLineMode.Backward: - for (int index = startIndex; index >= 0; --index) + case BreakLineMode.Backward: + for (int index = startIndex; index >= 0; --index) + { + if (char.IsWhiteSpace(span[index])) { - if (char.IsWhiteSpace(span[index])) - { - return (index, index + 1); - } + return (index, index + 1); } + } - break; + break; - case BreakLineMode.Forward: - for (int index = 0; index <= startIndex; ++index) + case BreakLineMode.Forward: + for (int index = 0; index <= startIndex; ++index) + { + if (char.IsWhiteSpace(span[index])) { - if (char.IsWhiteSpace(span[index])) - { - return (index, index + 1); - } + return (index, index + 1); } - - break; } - return null; + break; } + + return null; } } diff --git a/src/Ookii.CommandLine/StringSpanTuple.cs b/src/Ookii.CommandLine/StringSpanTuple.cs index 85c7ccaa..834a4ea2 100644 --- a/src/Ookii.CommandLine/StringSpanTuple.cs +++ b/src/Ookii.CommandLine/StringSpanTuple.cs @@ -1,23 +1,22 @@ using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +// Since ReadOnlySpan is a ref struct, it cannot be used in a regular tuple. +internal ref struct StringSpanTuple { - // Since ReadOnlySpan is a ref struct, it cannot be used in a regular tuple. - internal ref struct StringSpanTuple + public StringSpanTuple(ReadOnlySpan span1, ReadOnlySpan span2) { - public StringSpanTuple(ReadOnlySpan span1, ReadOnlySpan span2) - { - Span1 = span1; - Span2 = span2; - } + Span1 = span1; + Span2 = span2; + } - public ReadOnlySpan Span1; - public ReadOnlySpan Span2; + public ReadOnlySpan Span1; + public ReadOnlySpan Span2; - public void Deconstruct(out ReadOnlySpan span1, out ReadOnlySpan span2) - { - span1 = Span1; - span2 = Span2; - } + public void Deconstruct(out ReadOnlySpan span1, out ReadOnlySpan span2) + { + span1 = Span1; + span2 = Span2; } } diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs new file mode 100644 index 00000000..c40bd54d --- /dev/null +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -0,0 +1,123 @@ +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ookii.CommandLine.Support; + +/// +/// A source of arguments for the . +/// +/// +/// This class is used by the source generator when using the +/// attribute. It should not normally be used by other code. +/// +/// +public abstract class ArgumentProvider +{ + private readonly IEnumerable _validators; + + /// + /// Initializes a new instance of the class. + /// + /// The type that will hold the argument values. + /// + /// The for the arguments type, or + /// if there is none. + /// + /// The class validators for the arguments type. + protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, IEnumerable? validators) + { + ArgumentsType = argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)); + OptionsAttribute = options; + _validators = validators ?? Enumerable.Empty(); + } + + /// + /// Gets the kind of argument provider. + /// + /// + /// One of the values of the enumeration. + /// + public virtual ProviderKind Kind => ProviderKind.Unknown; + + /// + /// Gets the type that will hold the argument values. + /// + /// + /// The of the class that will hold the argument values. + /// + public Type ArgumentsType { get; } + + /// + /// Gets the friendly name of the application. + /// + /// + /// The friendly name of the application. + /// + public abstract string ApplicationFriendlyName { get; } + + /// + /// Gets a description that is used when generating usage information. + /// + /// + /// The description of the command line application. + /// + public abstract string Description { get; } + + /// + /// Gets the that was applied to the arguments type. + /// + /// + /// An instance of the class, or if + /// the attribute was not present. + /// + public ParseOptionsAttribute? OptionsAttribute { get; } + + /// + /// Gets a value that indicates whether this arguments type is a subcommand. + /// + /// + /// if the arguments type is a subcommand; otherwise, . + /// + public abstract bool IsCommand { get; } + + /// + /// Gets the arguments defined by the arguments type. + /// + /// The that is parsing the arguments. + /// An enumeration of instances. + public abstract IEnumerable GetArguments(CommandLineParser parser); + + /// + /// Runs the class validators for the arguments type. + /// + /// The that is parsing the arguments. + /// + /// One of the validators failed. + /// + public void RunValidators(CommandLineParser parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } + + foreach (var validator in _validators) + { + validator.Validate(parser); + } + } + + /// + /// Creates an instance of the arguments type. + /// + /// The that is parsing the arguments. + /// + /// An array with the values of any arguments backed by required properties, or + /// if there are no required properties, or if the property equals + /// . + /// + /// An instance of the type indicated by the property. + public abstract object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues); +} diff --git a/src/Ookii.CommandLine/Support/CommandProvider.cs b/src/Ookii.CommandLine/Support/CommandProvider.cs new file mode 100644 index 00000000..4a71e4da --- /dev/null +++ b/src/Ookii.CommandLine/Support/CommandProvider.cs @@ -0,0 +1,38 @@ +using Ookii.CommandLine.Commands; +using System.Collections.Generic; + +namespace Ookii.CommandLine.Support; + +/// +/// A source of commands for the . +/// +/// +/// This class is used by the source generator when using the +/// attribute. It should not normally be used by other code. +/// +/// +public abstract class CommandProvider +{ + /// + /// Gets the kind of command provider. + /// + /// + /// One of the values of the enumeration. + /// + public virtual ProviderKind Kind => ProviderKind.Unknown; + + /// + /// Gets all the commands supported by this provider. + /// + /// The that the commands belong to. + /// + /// A list of instances for the commands, in arbitrary order. + /// + public abstract IEnumerable GetCommandsUnsorted(CommandManager manager); + + /// + /// Gets the application description. + /// + /// The application description, or if there is none. + public abstract string? GetApplicationDescription(); +} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs new file mode 100644 index 00000000..d866abce --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -0,0 +1,187 @@ +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; + +namespace Ookii.CommandLine.Support; + +/// +/// Represents information about an argument determined by the source generator. +/// +/// +/// This class is used by the source generator when using the +/// attribute. It should not normally be used by other code. +/// +/// +public class GeneratedArgument : CommandLineArgument +{ + private readonly Action? _setProperty; + private readonly Func? _getProperty; + private readonly Func? _callMethod; + private readonly string _defaultValueDescription; + private readonly string? _defaultKeyDescription; + + private GeneratedArgument(ArgumentInfo info, Action? setProperty, Func? getProperty, + Func? callMethod, string defaultValueDescription, string? defaultKeyDescription) : base(info) + { + _setProperty = setProperty; + _getProperty = getProperty; + _callMethod = callMethod; + _defaultValueDescription = defaultValueDescription; + _defaultKeyDescription = defaultKeyDescription; + } + + /// + /// Creates a instance. + /// + /// The this argument belongs to. + /// The type of the argument. + /// The element type including . + /// The element type excluding . + /// The name of the property or method. + /// The . + /// The kind of argument. + /// The for the argument's type. + /// Indicates if values are allowed. + /// + /// The value description to use if the attribute is + /// not present. For dictionary arguments, this is the value description for the value of the + /// key/value pair. + /// + /// The position for positional arguments that use automatic positioning. + /// + /// The value description to use for the key of a dictionary argument if the + /// attribute is not present. + /// + /// + /// Indicates if the argument used a C# 11 required property. + /// + /// + /// Default value to use if the property + /// is not set. + /// + /// The type of the key of a dictionary argument. + /// The type of the value of a dictionary argument. + /// The . + /// The . + /// The . + /// The . + /// The . + /// A collection of values. + /// A collection of values. + /// A collection of values. + /// + /// A delegate that sets the value of the property that defined the argument. + /// + /// + /// A delegate that gets the value of the property that defined the argument. + /// + /// + /// A delegate that calls the method that defined the argument. + /// + /// A instance. + public static GeneratedArgument Create(CommandLineParser parser, + Type argumentType, + Type elementTypeWithNullable, + Type elementType, + string memberName, + CommandLineArgumentAttribute attribute, + ArgumentKind kind, + ArgumentConverter converter, + bool allowsNull, + string defaultValueDescription, + int? position = null, + string? defaultKeyDescription = null, + bool requiredProperty = false, + object? alternateDefaultValue = null, + Type? keyType = null, + Type? valueType = null, + MultiValueSeparatorAttribute? multiValueSeparatorAttribute = null, + DescriptionAttribute? descriptionAttribute = null, + ValueDescriptionAttribute? valueDescriptionAttribute = null, + bool allowDuplicateDictionaryKeys = false, + KeyValueSeparatorAttribute? keyValueSeparatorAttribute = null, + IEnumerable? aliasAttributes = null, + IEnumerable? shortAliasAttributes = null, + IEnumerable? validationAttributes = null, + Action? setProperty = null, + Func? getProperty = null, + Func? callMethod = null) + { + if (position is int pos) + { + Debug.Assert(attribute.IsPositional && attribute.Position < 0); + attribute.Position = pos; + } + + var info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, memberName, attribute, + descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); + + info.ElementType = elementType; + info.ElementTypeWithNullable = elementTypeWithNullable; + info.Converter = converter; + info.Kind = kind; + info.DefaultValue ??= alternateDefaultValue; + if (info.Kind is ArgumentKind.MultiValue or ArgumentKind.Dictionary) + { + info.MultiValueInfo = GetMultiValueInfo(multiValueSeparatorAttribute); + if (info.Kind == ArgumentKind.Dictionary) + { + info.DictionaryInfo = new(allowDuplicateDictionaryKeys, keyType!, valueType!, + keyValueSeparatorAttribute?.Separator ?? KeyValuePairConverter.DefaultSeparator); + } + } + + return new GeneratedArgument(info, setProperty, getProperty, callMethod, defaultValueDescription, defaultKeyDescription); + } + + /// + protected override bool CanSetProperty => _setProperty != null; + + /// + protected override CancelMode CallMethod(object? value) + { + if (_callMethod == null) + { + throw new InvalidOperationException(Properties.Resources.InvalidMethodAccess); + } + + return _callMethod(value, this.Parser); + } + + /// + protected override object? GetProperty(object target) + { + if (_getProperty == null) + { + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + } + + return _getProperty(target); + } + + /// + protected override void SetProperty(object target, object? value) + { + if (_setProperty == null) + { + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + } + + _setProperty(target, value); + } + + /// + protected override string DetermineValueDescriptionForType(Type type) + { + Debug.Assert(DictionaryInfo == null ? type == ElementType : (type == DictionaryInfo.KeyType || type == DictionaryInfo.ValueType)); + if (DictionaryInfo != null && type == DictionaryInfo.KeyType) + { + return _defaultKeyDescription!; + } + + return _defaultValueDescription; + } +} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs new file mode 100644 index 00000000..e2d4b446 --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -0,0 +1,61 @@ +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace Ookii.CommandLine.Support; + +/// +/// A base class for argument providers created by the . +/// +/// +/// This class is used by the source generator when using the +/// attribute. It should not normally be used by other code. +/// +/// +public abstract class GeneratedArgumentProvider : ArgumentProvider +{ + private readonly ApplicationFriendlyNameAttribute? _friendlyNameAttribute; + private readonly DescriptionAttribute? _descriptionAttribute; + + /// + /// Initializes a new instance of the class. + /// + /// The type that will hold the argument values. + /// + /// The for the arguments type, or if + /// there is none. + /// + /// The class validators for the arguments type. + /// + /// The for the arguments type, or + /// if there is none. + /// + /// + /// The for the arguments type, or if + /// there is none. + /// + protected GeneratedArgumentProvider(Type argumentsType, + ParseOptionsAttribute? options = null, + IEnumerable? validators = null, + ApplicationFriendlyNameAttribute? friendlyName = null, + DescriptionAttribute? description = null) + : base(argumentsType, options, validators) + { + _friendlyNameAttribute = friendlyName; + _descriptionAttribute = description; + } + + /// + public override ProviderKind Kind => ProviderKind.Generated; + + /// + public override string ApplicationFriendlyName + => _friendlyNameAttribute?.Name ?? ArgumentsType.Assembly.GetCustomAttribute()?.Name + ?? ArgumentsType.Assembly.GetCustomAttribute()?.Title ?? ArgumentsType.Assembly.GetName().Name + ?? string.Empty; + + /// + public override string Description => _descriptionAttribute?.Description ?? string.Empty; +} diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs new file mode 100644 index 00000000..3d2f812c --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs @@ -0,0 +1,70 @@ +using Ookii.CommandLine.Commands; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace Ookii.CommandLine.Support; + +/// +/// Represents information about a subcommand determined by the source generator. +/// +/// +/// This class is used by the source generator when using the +/// attribute. It should not normally be used by other code. +/// +/// +public class GeneratedCommandInfo : CommandInfo +{ + private readonly DescriptionAttribute? _descriptionAttribute; + private readonly IEnumerable? _aliases; + private readonly Func? _createParser; + + /// + /// Initializes a new instance of the class. + /// + /// The command manager. + /// The type of the command. + /// The . + /// The . + /// A collection of values. + /// A delegate that creates a command line parser for the command when invoked. + /// The type of the parent command. + public GeneratedCommandInfo(CommandManager manager, + Type commandType, + CommandAttribute attribute, + DescriptionAttribute? descriptionAttribute = null, + IEnumerable? aliasAttributes = null, + Func? createParser = null, + Type? parentCommandType = null) + : base(commandType, attribute, manager, parentCommandType) + { + _descriptionAttribute = descriptionAttribute; + _aliases = aliasAttributes?.Select(a => a.Alias); + _createParser = createParser; + } + + /// + public override string? Description => _descriptionAttribute?.Description; + + /// + public override bool UseCustomArgumentParsing => false; + + /// + public override IEnumerable Aliases => _aliases ?? Enumerable.Empty(); + + /// + public override CommandLineParser CreateParser() + { + if (_createParser == null) + { + throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); + } + + return _createParser(Manager.Options); + } + + /// + public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() + => throw new InvalidOperationException(Properties.Resources.NoCustomParsing); +} diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs new file mode 100644 index 00000000..4b9f5f4e --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs @@ -0,0 +1,43 @@ +using Ookii.CommandLine.Commands; +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Ookii.CommandLine.Support; + +/// +/// Represents information about a subcommand that uses the +/// interface, determined by the source generator. +/// +/// The command class. +/// +/// This class is used by the source generator when using the +/// attribute. It should not normally be used by other code. +/// +/// +public class GeneratedCommandInfoWithCustomParsing : GeneratedCommandInfo + where T : class, ICommandWithCustomParsing, new() +{ + /// + /// Initializes a new instance of the class. + /// + /// The command manager. + /// The . + /// The . + /// A collection of values. + /// The type of the parent command. + public GeneratedCommandInfoWithCustomParsing(CommandManager manager, + CommandAttribute attribute, + DescriptionAttribute? descriptionAttribute = null, + IEnumerable? aliasAttributes = null, + Type? parentCommandType = null) + : base(manager, typeof(T), attribute, descriptionAttribute, aliasAttributes, parentCommandType: parentCommandType) + { + } + + /// + public override bool UseCustomArgumentParsing => true; + + /// + public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() => new T(); +} diff --git a/src/Ookii.CommandLine/Support/ProviderKind.cs b/src/Ookii.CommandLine/Support/ProviderKind.cs new file mode 100644 index 00000000..eb2d98a2 --- /dev/null +++ b/src/Ookii.CommandLine/Support/ProviderKind.cs @@ -0,0 +1,22 @@ +namespace Ookii.CommandLine.Support; + +/// +/// Specifies the kind of provider that was the source of the arguments or subcommands. +/// +public enum ProviderKind +{ + /// + /// A custom provider that was not part of Ookii.CommandLine. + /// + Unknown, + /// + /// An provider that uses reflection. + /// + Reflection, + /// + /// An provider that uses source generation. These are typically created using the + /// and + /// attributes. + /// + Generated +} diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs new file mode 100644 index 00000000..f01b610b --- /dev/null +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -0,0 +1,419 @@ +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Ookii.CommandLine.Support; + +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif +internal class ReflectionArgument : CommandLineArgument +{ + #region Nested types + + private struct MethodArgumentInfo + { + public MethodInfo Method { get; set; } + public bool HasValueParameter { get; set; } + public bool HasParserParameter { get; set; } + } + + #endregion + + private readonly PropertyInfo? _property; + private readonly MethodArgumentInfo? _method; + + private ReflectionArgument(ArgumentInfo info, PropertyInfo? property, MethodArgumentInfo? method) + : base(info) + { + _property = property; + _method = method; + } + + protected override bool CanSetProperty => _property?.GetSetMethod() != null; + + protected override void SetProperty(object target, object? value) + { + if (_property == null) + { + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + } + + _property.SetValue(target, value); + } + + protected override object? GetProperty(object target) + { + if (_property == null) + { + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + } + + return _property.GetValue(target); + } + + protected override CancelMode CallMethod(object? value) + { + if (_method is not MethodArgumentInfo info) + { + throw new InvalidOperationException(Properties.Resources.InvalidMethodAccess); + } + + int parameterCount = (info.HasValueParameter ? 1 : 0) + (info.HasParserParameter ? 1 : 0); + var parameters = new object?[parameterCount]; + int index = 0; + if (info.HasValueParameter) + { + parameters[index] = Value; + ++index; + } + + if (info.HasParserParameter) + { + parameters[index] = Parser; + } + + return info.Method.Invoke(null, parameters) switch + { + CancelMode mode => mode, + false => CancelMode.Abort, + _ => CancelMode.None + }; + } + + internal static CommandLineArgument Create(CommandLineParser parser, PropertyInfo property) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } + + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + return Create(parser, property, null, property.PropertyType, DetermineAllowsNull(property)); + } + + internal static CommandLineArgument Create(CommandLineParser parser, MethodInfo method) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } + + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + var (methodInfo, argumentType, allowsNull) = DetermineMethodArgumentInfo(method) + ?? throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.InvalidMethodSignatureFormat, method.Name)); + + return Create(parser, null, methodInfo, argumentType, allowsNull); + } + + private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo? property, MethodArgumentInfo? method, + Type argumentType, bool allowsNull) + { + var member = ((MemberInfo?)property ?? method?.Method)!; + var attribute = member.GetCustomAttribute() + ?? throw new ArgumentException(Properties.Resources.MissingArgumentAttribute, nameof(method)); + + if (attribute.IsPositional && attribute.Position < 0) + { + throw new NotSupportedException(Properties.Resources.AutoPositionNotSupportedFormat); + } + + var multiValueSeparatorAttribute = member.GetCustomAttribute(); + var descriptionAttribute = member.GetCustomAttribute(); + var valueDescriptionAttribute = member.GetCustomAttribute(); + var allowDuplicateDictionaryKeys = Attribute.IsDefined(member, typeof(AllowDuplicateDictionaryKeysAttribute)); + var keyValueSeparatorAttribute = member.GetCustomAttribute(); + var aliasAttributes = member.GetCustomAttributes(); + var shortAliasAttributes = member.GetCustomAttributes(); + var validationAttributes = member.GetCustomAttributes(); +#if NET7_0_OR_GREATER + var requiredProperty = Attribute.IsDefined(member, typeof(RequiredMemberAttribute)); +#else + var requiredProperty = false; +#endif + + ArgumentInfo info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, member.Name, attribute, + descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); + + DetermineAdditionalInfo(ref info, member, multiValueSeparatorAttribute, keyValueSeparatorAttribute, + allowDuplicateDictionaryKeys); + + return new ReflectionArgument(info, property, method); + } + + private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member, + MultiValueSeparatorAttribute? multiValueSeparatorAttribute, KeyValueSeparatorAttribute? keyValueSeparatorAttribute, + bool allowDuplicateDictionaryKeys) + { + var converterAttribute = member.GetCustomAttribute(); + var keyArgumentConverterAttribute = member.GetCustomAttribute(); + var valueArgumentConverterAttribute = member.GetCustomAttribute(); + var converterType = converterAttribute?.GetConverterType(); + + if (member is PropertyInfo property) + { + var (collectionType, dictionaryType, elementType) = + DetermineMultiValueType(info.ArgumentName, info.ArgumentType, property); + + if (dictionaryType != null) + { + Debug.Assert(elementType != null); + info.Kind = ArgumentKind.Dictionary; + info.MultiValueInfo = GetMultiValueInfo(multiValueSeparatorAttribute); + info.ElementTypeWithNullable = elementType!; + info.AllowNull = DetermineDictionaryValueTypeAllowsNull(dictionaryType, property); + var genericArguments = dictionaryType.GetGenericArguments(); + info.DictionaryInfo = new(allowDuplicateDictionaryKeys, genericArguments[0], genericArguments[1], + keyValueSeparatorAttribute?.Separator ?? KeyValuePairConverter.DefaultSeparator); + + if (converterType == null) + { + converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); + var keyConverter = info.DictionaryInfo.KeyType.GetStringConverter(keyArgumentConverterAttribute?.GetConverterType()); + var valueConverter = info.DictionaryInfo.ValueType.GetStringConverter(valueArgumentConverterAttribute?.GetConverterType()); + info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, keyConverter, valueConverter, + info.DictionaryInfo.KeyValueSeparator, info.AllowNull)!; + } + } + else if (collectionType != null) + { + Debug.Assert(elementType != null); + info.Kind = ArgumentKind.MultiValue; + info.MultiValueInfo = GetMultiValueInfo(multiValueSeparatorAttribute); + info.ElementTypeWithNullable = elementType!; + info.AllowNull = DetermineCollectionElementTypeAllowsNull(collectionType, property); + } + } + else + { + info.Kind = ArgumentKind.Method; + } + + // If it's a Nullable, now get the underlying type. + info.ElementType = info.ElementTypeWithNullable.GetUnderlyingType(); + + // Use the original Nullable for this if it is one. + info.Converter ??= info.ElementTypeWithNullable.GetStringConverter(converterType); + } + + // Returns a tuple of (collectionType, dictionaryType, elementType) + private static (Type?, Type?, Type?) DetermineMultiValueType(string argumentName, Type argumentType, PropertyInfo property) + { + // If the type is Dictionary it doesn't matter if the property is + // read-only or not. + if (argumentType.IsGenericType && argumentType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + var elementType = typeof(KeyValuePair<,>).MakeGenericType(argumentType.GetGenericArguments()); + return (null, argumentType, elementType); + } + + if (argumentType.IsArray) + { + if (argumentType.GetArrayRank() != 1) + { + throw new NotSupportedException(Properties.Resources.InvalidArrayRank); + } + + if (property.GetSetMethod() == null) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.PropertyIsReadOnlyFormat, argumentName)); + } + + var elementType = argumentType.GetElementType()!; + return (argumentType, null, elementType); + } + + // The interface approach requires a read-only property. If it's read-write, treat it + // like a non-multi-value argument. + // Don't use CanWrite because that returns true for properties with a private set + // accessor. + if (property.GetSetMethod() != null) + { + return (null, null, null); + } + + var dictionaryType = argumentType.FindGenericInterface(typeof(IDictionary<,>)); + if (dictionaryType != null) + { + var elementType = typeof(KeyValuePair<,>).MakeGenericType(dictionaryType.GetGenericArguments()); + return (null, dictionaryType, elementType); + } + + var collectionType = argumentType.FindGenericInterface(typeof(ICollection<>)); + if (collectionType != null) + { + var elementType = collectionType.GetGenericArguments()[0]; + return (collectionType, null, elementType); + } + + // This is a read-only property with an unsupported type. + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.PropertyIsReadOnlyFormat, argumentName)); + } + + private static bool DetermineDictionaryValueTypeAllowsNull(Type type, PropertyInfo property) + { + var valueTypeNull = DetermineValueTypeNullable(type.GetGenericArguments()[1]); + if (valueTypeNull != null) + { + return valueTypeNull.Value; + } + +#if NET6_0_OR_GREATER + // Type is the IDictionary<,> implemented interface, not the actual type of the property + // or parameter, which is what we need here. + var actualType = property.PropertyType; + + // We can only determine the nullability state if the property or parameter's actual + // type is Dictionary<,> or IDictionary<,>. Otherwise, we just assume nulls are + // allowed. + if (actualType != null && actualType.IsGenericType && + (actualType.GetGenericTypeDefinition() == typeof(Dictionary<,>) || actualType.GetGenericTypeDefinition() == typeof(IDictionary<,>))) + { + var context = new NullabilityInfoContext(); + var info = context.Create(property); + return info.GenericTypeArguments[1].ReadState != NullabilityState.NotNull; + } +#endif + + return true; + } + + private static bool DetermineCollectionElementTypeAllowsNull(Type type, PropertyInfo property) + { + Type elementType = type.IsArray ? type.GetElementType()! : type.GetGenericArguments()[0]; + var valueTypeNull = DetermineValueTypeNullable(elementType); + if (valueTypeNull != null) + { + return valueTypeNull.Value; + } + +#if NET6_0_OR_GREATER + // Type is the ICollection<> implemented interface, not the actual type of the property + // or parameter, which is what we need here. + var actualType = property.PropertyType; + + // We can only determine the nullability state if the property or parameter's actual + // type is an array or ICollection<>. Otherwise, we just assume nulls are allowed. + if (actualType != null && (actualType.IsArray || actualType.IsGenericType && + actualType.GetGenericTypeDefinition() == typeof(ICollection<>))) + { + var context = new NullabilityInfoContext(); + var info = context.Create(property); + if (actualType.IsArray) + { + return info.ElementType?.ReadState != NullabilityState.NotNull; + } + else + { + return info.GenericTypeArguments[0].ReadState != NullabilityState.NotNull; + } + } +#endif + + return true; + } + + private static bool DetermineAllowsNull(PropertyInfo property) + { + var valueTypeNull = DetermineValueTypeNullable(property.PropertyType); + if (valueTypeNull != null) + { + return valueTypeNull.Value; + } + +#if NET6_0_OR_GREATER + var context = new NullabilityInfoContext(); + var info = context.Create(property); + return info.WriteState != NullabilityState.NotNull; +#else + return true; +#endif + } + + private static bool DetermineAllowsNull(ParameterInfo parameter) + { + var valueTypeNull = DetermineValueTypeNullable(parameter.ParameterType); + if (valueTypeNull != null) + { + return valueTypeNull.Value; + } + +#if NET6_0_OR_GREATER + var context = new NullabilityInfoContext(); + var info = context.Create(parameter); + return info.WriteState != NullabilityState.NotNull; +#else + return true; +#endif + } + + private static bool? DetermineValueTypeNullable(Type type) + { + if (type.IsValueType) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + return null; + } + + private static (MethodArgumentInfo, Type, bool)? DetermineMethodArgumentInfo(MethodInfo method) + { + var parameters = method.GetParameters(); + if (!method.IsStatic || + (method.ReturnType != typeof(bool) && method.ReturnType != typeof(void) && method.ReturnType != typeof(CancelMode)) || + parameters.Length > 2) + { + return null; + } + + bool allowsNull = false; + var argumentType = typeof(bool); + var info = new MethodArgumentInfo() { Method = method }; + if (parameters.Length == 2) + { + argumentType = parameters[0].ParameterType; + if (parameters[1].ParameterType != typeof(CommandLineParser)) + { + return null; + } + + info.HasValueParameter = true; + info.HasParserParameter = true; + } + else if (parameters.Length == 1) + { + if (parameters[0].ParameterType == typeof(CommandLineParser)) + { + info.HasParserParameter = true; + } + else + { + argumentType = parameters[0].ParameterType; + info.HasValueParameter = true; + } + } + + if (info.HasValueParameter) + { + allowsNull = DetermineAllowsNull(parameters[0]); + } + + return (info, argumentType, allowsNull); + } +} diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs new file mode 100644 index 00000000..8aac2a0e --- /dev/null +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -0,0 +1,65 @@ +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace Ookii.CommandLine.Support; + +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif +internal class ReflectionArgumentProvider : ArgumentProvider +{ + public ReflectionArgumentProvider(Type type) + : base(type, type.GetCustomAttribute(), type.GetCustomAttributes()) + { + } + + public override ProviderKind Kind => ProviderKind.Reflection; + + public override string ApplicationFriendlyName + { + get + { + var attribute = ArgumentsType.GetCustomAttribute() ?? + ArgumentsType.Assembly.GetCustomAttribute(); + + return attribute?.Name ?? ArgumentsType.Assembly.GetCustomAttribute()?.Title ?? + ArgumentsType.Assembly.GetName().Name ?? string.Empty; + } + } + + public override string Description => ArgumentsType.GetCustomAttribute()?.Description ?? string.Empty; + + public override bool IsCommand => CommandInfo.IsCommand(ArgumentsType); + + public override object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues) + { + var inject = ArgumentsType.GetConstructor(new[] { typeof(CommandLineParser) }) != null; + if (inject) + { + return Activator.CreateInstance(ArgumentsType, parser)!; + } + else + { + return Activator.CreateInstance(ArgumentsType)!; + } + } + + public override IEnumerable GetArguments(CommandLineParser parser) + { + var properties = ArgumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => Attribute.IsDefined(p, typeof(CommandLineArgumentAttribute))) + .Select(p => ReflectionArgument.Create(parser, p)); + + var methods = ArgumentsType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => Attribute.IsDefined(m, typeof(CommandLineArgumentAttribute))) + .Select(m => ReflectionArgument.Create(parser, m)); + + return properties.Concat(methods); + } +} diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs new file mode 100644 index 00000000..98e61033 --- /dev/null +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -0,0 +1,85 @@ +using Ookii.CommandLine.Commands; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace Ookii.CommandLine.Support; + + +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif +internal class ReflectionCommandInfo : CommandInfo +{ + private string? _description; + + public ReflectionCommandInfo(Type commandType, CommandAttribute? attribute, CommandManager manager) + : base(commandType, attribute ?? GetCommandAttributeOrThrow(commandType), manager, GetParentCommand(commandType)) + { + } + + public override string? Description => _description ??= GetCommandDescription(); + + public override bool UseCustomArgumentParsing => CommandType.ImplementsInterface(typeof(ICommandWithCustomParsing)); + + public override IEnumerable Aliases => CommandType.GetCustomAttributes().Select(a => a.Alias); + + public static new CommandInfo? TryCreate(Type commandType, CommandManager manager) + { + var attribute = GetCommandAttribute(commandType); + if (attribute == null) + { + return null; + } + + return new ReflectionCommandInfo(commandType, attribute, manager); + } + + public override CommandLineParser CreateParser() + { + if (UseCustomArgumentParsing) + { + throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); + } + + return new CommandLineParser(CommandType, Manager.Options); + } + + public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() + { + if (!UseCustomArgumentParsing) + { + throw new InvalidOperationException(Properties.Resources.NoCustomParsing); + } + + return (ICommandWithCustomParsing)Activator.CreateInstance(CommandType)!; + } + + private static CommandAttribute GetCommandAttributeOrThrow(Type commandType) + { + return GetCommandAttribute(commandType) ?? + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, + Properties.Resources.TypeIsNotCommandFormat, commandType.FullName)); + } + + private string? GetCommandDescription() + { + return CommandType.GetCustomAttribute()?.Description; + } + + private static Type? GetParentCommand(Type commandType) + { + if (commandType == null) + { + throw new ArgumentNullException(nameof(commandType)); + } + + var attribute = commandType.GetCustomAttribute(); + return attribute?.GetParentCommandType(); + } +} diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs new file mode 100644 index 00000000..9ed1b1f9 --- /dev/null +++ b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs @@ -0,0 +1,62 @@ +using Ookii.CommandLine.Commands; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace Ookii.CommandLine.Support; + +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif +internal class ReflectionCommandProvider : CommandProvider +{ + private readonly Assembly? _assembly; + private readonly IEnumerable? _assemblies; + private readonly Assembly _callingAssembly; + + public ReflectionCommandProvider(Assembly assembly, Assembly callingAssembly) + { + _assembly = assembly; + _callingAssembly = callingAssembly; + } + + public ReflectionCommandProvider(IEnumerable assemblies, Assembly callingAssembly) + { + _assemblies = assemblies; + _callingAssembly = callingAssembly; + if (_assemblies.Any(a => a == null)) + { + throw new ArgumentNullException(nameof(assemblies)); + } + } + + public override ProviderKind Kind => ProviderKind.Reflection; + + public override IEnumerable GetCommandsUnsorted(CommandManager manager) + { + { + IEnumerable types; + if (_assembly != null) + { + types = _assembly.GetTypes(); + } + else + { + Debug.Assert(_assemblies != null); + types = _assemblies.SelectMany(a => a.GetTypes()); + } + + return from type in types + where type.Assembly == _callingAssembly || type.IsPublic + let info = CommandInfo.TryCreate(type, manager) + where info != null + select info; + } + } + + public override string? GetApplicationDescription() + => (_assembly ?? _assemblies?.FirstOrDefault())?.GetCustomAttribute()?.Description; +} diff --git a/src/Ookii.CommandLine/Terminal/StandardStream.cs b/src/Ookii.CommandLine/Terminal/StandardStream.cs index 2e4f74fb..5ee3ba3f 100644 --- a/src/Ookii.CommandLine/Terminal/StandardStream.cs +++ b/src/Ookii.CommandLine/Terminal/StandardStream.cs @@ -1,21 +1,20 @@ -namespace Ookii.CommandLine.Terminal +namespace Ookii.CommandLine.Terminal; + +/// +/// Represents one of the standard console streams. +/// +public enum StandardStream { /// - /// Represents one of the standard console streams. + /// The standard output stream. /// - public enum StandardStream - { - /// - /// The standard output stream. - /// - Output, - /// - /// The standard input stream. - /// - Input, - /// - /// The standard error stream. - /// - Error - } + Output, + /// + /// The standard input stream. + /// + Input, + /// + /// The standard error stream. + /// + Error } diff --git a/src/Ookii.CommandLine/Terminal/TextFormat.cs b/src/Ookii.CommandLine/Terminal/TextFormat.cs index a83671b6..23ff7f62 100644 --- a/src/Ookii.CommandLine/Terminal/TextFormat.cs +++ b/src/Ookii.CommandLine/Terminal/TextFormat.cs @@ -1,191 +1,291 @@ using System; using System.Drawing; -namespace Ookii.CommandLine.Terminal +namespace Ookii.CommandLine.Terminal; + +/// +/// Represents a virtual terminal (VT) sequence for a change in text formatting. +/// +/// +/// +/// Write one of the predefined values in this structure to a stream representing the console, +/// such as or , to set the specified text +/// format on that stream. +/// +/// +/// You should only write VT sequences to the console if they are supported. Use the +/// method to check whether VT sequences are supported, +/// and to enable them if required by the operating system. +/// +/// +/// You can combine instances to apply multiple options by using +/// the method or the operator. +/// +/// +/// +public readonly struct TextFormat : IEquatable { /// - /// Provides constants for various virtual terminal sequences that control text format. + /// Resets the text format to the settings before modification. + /// + public static readonly TextFormat Default = new("\x1b[0m"); + /// + /// Applies the brightness/intensity flag to the foreground color. + /// + public static readonly TextFormat BoldBright = new("\x1b[1m"); + /// + /// Removes the brightness/intensity flag from the foreground color. + /// + public static readonly TextFormat NoBoldBright = new("\x1b[22m"); + /// + /// Adds underlining to the text. + /// + public static readonly TextFormat Underline = new("\x1b[4m"); + /// + /// Removes underlining from the text. + /// + public static readonly TextFormat NoUnderline = new("\x1b[24m"); + /// + /// Swaps the foreground and background colors. + /// + public static readonly TextFormat Negative = new("\x1b[7m"); + /// + /// Returns the foreground and background colors to their normal, non-swapped state. + /// + public static readonly TextFormat Positive = new("\x1b[27m"); + /// + /// Sets the foreground color to Black. + /// + public static readonly TextFormat ForegroundBlack = new("\x1b[30m"); + /// + /// Sets the foreground color to Red. + /// + public static readonly TextFormat ForegroundRed = new("\x1b[31m"); + /// + /// Sets the foreground color to Green. + /// + public static readonly TextFormat ForegroundGreen = new("\x1b[32m"); + /// + /// Sets the foreground color to Yellow. + /// + public static readonly TextFormat ForegroundYellow = new("\x1b[33m"); + /// + /// Sets the foreground color to Blue. + /// + public static readonly TextFormat ForegroundBlue = new("\x1b[34m"); + /// + /// Sets the foreground color to Magenta. + /// + public static readonly TextFormat ForegroundMagenta = new("\x1b[35m"); + /// + /// Sets the foreground color to Cyan. + /// + public static readonly TextFormat ForegroundCyan = new("\x1b[36m"); + /// + /// Sets the foreground color to White. + /// + public static readonly TextFormat ForegroundWhite = new("\x1b[37m"); + /// + /// Sets the foreground color to Default. + /// + public static readonly TextFormat ForegroundDefault = new("\x1b[39m"); + /// + /// Sets the background color to Black. + /// + public static readonly TextFormat BackgroundBlack = new("\x1b[40m"); + /// + /// Sets the background color to Red. + /// + public static readonly TextFormat BackgroundRed = new("\x1b[41m"); + /// + /// Sets the background color to Green. + /// + public static readonly TextFormat BackgroundGreen = new("\x1b[42m"); + /// + /// Sets the background color to Yellow. + /// + public static readonly TextFormat BackgroundYellow = new("\x1b[43m"); + /// + /// Sets the background color to Blue. + /// + public static readonly TextFormat BackgroundBlue = new("\x1b[44m"); + /// + /// Sets the background color to Magenta. + /// + public static readonly TextFormat BackgroundMagenta = new("\x1b[45m"); + /// + /// Sets the background color to Cyan. + /// + public static readonly TextFormat BackgroundCyan = new("\x1b[46m"); + /// + /// Sets the background color to White. + /// + public static readonly TextFormat BackgroundWhite = new("\x1b[47m"); + /// + /// Sets the background color to Default. + /// + public static readonly TextFormat BackgroundDefault = new("\x1b[49m"); + /// + /// Sets the foreground color to bright Black. + /// + public static readonly TextFormat BrightForegroundBlack = new("\x1b[90m"); + /// + /// Sets the foreground color to bright Red. + /// + public static readonly TextFormat BrightForegroundRed = new("\x1b[91m"); + /// + /// Sets the foreground color to bright Green. + /// + public static readonly TextFormat BrightForegroundGreen = new("\x1b[92m"); + /// + /// Sets the foreground color to bright Yellow. /// - public static class TextFormat + public static readonly TextFormat BrightForegroundYellow = new("\x1b[93m"); + /// + /// Sets the foreground color to bright Blue. + /// + public static readonly TextFormat BrightForegroundBlue = new("\x1b[94m"); + /// + /// Sets the foreground color to bright Magenta. + /// + public static readonly TextFormat BrightForegroundMagenta = new("\x1b[95m"); + /// + /// Sets the foreground color to bright Cyan. + /// + public static readonly TextFormat BrightForegroundCyan = new("\x1b[96m"); + /// + /// Sets the foreground color to bright White. + /// + public static readonly TextFormat BrightForegroundWhite = new("\x1b[97m"); + /// + /// Sets the background color to bright Black. + /// + public static readonly TextFormat BrightBackgroundBlack = new("\x1b[100m"); + /// + /// Sets the background color to bright Red. + /// + public static readonly TextFormat BrightBackgroundRed = new("\x1b[101m"); + /// + /// Sets the background color to bright Green. + /// + public static readonly TextFormat BrightBackgroundGreen = new("\x1b[102m"); + /// + /// Sets the background color to bright Yellow. + /// + public static readonly TextFormat BrightBackgroundYellow = new("\x1b[103m"); + /// + /// Sets the background color to bright Blue. + /// + public static readonly TextFormat BrightBackgroundBlue = new("\x1b[104m"); + /// + /// Sets the background color to bright Magenta. + /// + public static readonly TextFormat BrightBackgroundMagenta = new("\x1b[105m"); + /// + /// Sets the background color to bright Cyan. + /// + public static readonly TextFormat BrightBackgroundCyan = new("\x1b[106m"); + /// + /// Sets the background color to bright White. + /// + public static readonly TextFormat BrightBackgroundWhite = new("\x1b[107m"); + + private readonly string? _value; + + /// + /// Returns a virtual terminal sequence that can be used to set the foreground or background + /// color to an RGB color. + /// + /// The color to use. + /// + /// to apply the color to the background; otherwise, it's applied + /// to the background. + /// + /// A instance with the virtual terminal sequence. + public static TextFormat GetExtendedColor(Color color, bool foreground = true) { - /// - /// Resets the text format to the settings before modification. - /// - public const string Default = "\x1b[0m"; - /// - /// Applies the brightness/intensity flag to the foreground color. - /// - public const string BoldBright = "\x1b[1m"; - /// - /// Removes the brightness/intensity flag to the foreground color. - /// - public const string NoBoldBright = "\x1b[22m"; - /// - /// Adds underline. - /// - public const string Underline = "\x1b[4m"; - /// - /// Removes underline. - /// - public const string NoUnderline = "\x1b[24m"; - /// - /// Swaps foreground and background colors. - /// - public const string Negative = "\x1b[7m"; - /// - /// Returns foreground and background colors to normal. - /// - public const string Positive = "\x1b[27m"; - /// - /// Sets the foreground color to Black. - /// - public const string ForegroundBlack = "\x1b[30m"; - /// - /// Sets the foreground color to Red. - /// - public const string ForegroundRed = "\x1b[31m"; - /// - /// Sets the foreground color to Green. - /// - public const string ForegroundGreen = "\x1b[32m"; - /// - /// Sets the foreground color to Yellow. - /// - public const string ForegroundYellow = "\x1b[33m"; - /// - /// Sets the foreground color to Blue. - /// - public const string ForegroundBlue = "\x1b[34m"; - /// - /// Sets the foreground color to Magenta. - /// - public const string ForegroundMagenta = "\x1b[35m"; - /// - /// Sets the foreground color to Cyan. - /// - public const string ForegroundCyan = "\x1b[36m"; - /// - /// Sets the foreground color to White. - /// - public const string ForegroundWhite = "\x1b[37m"; - /// - /// Sets the foreground color to Default. - /// - public const string ForegroundDefault = "\x1b[39m"; - /// - /// Sets the background color to Black. - /// - public const string BackgroundBlack = "\x1b[40m"; - /// - /// Sets the background color to Red. - /// - public const string BackgroundRed = "\x1b[41m"; - /// - /// Sets the background color to Green. - /// - public const string BackgroundGreen = "\x1b[42m"; - /// - /// Sets the background color to Yellow. - /// - public const string BackgroundYellow = "\x1b[43m"; - /// - /// Sets the background color to Blue. - /// - public const string BackgroundBlue = "\x1b[44m"; - /// - /// Sets the background color to Magenta. - /// - public const string BackgroundMagenta = "\x1b[45m"; - /// - /// Sets the background color to Cyan. - /// - public const string BackgroundCyan = "\x1b[46m"; - /// - /// Sets the background color to White. - /// - public const string BackgroundWhite = "\x1b[47m"; - /// - /// Sets the background color to Default. - /// - public const string BackgroundDefault = "\x1b[49m"; - /// - /// Sets the foreground color to bright Black. - /// - public const string BrightForegroundBlack = "\x1b[90m"; - /// - /// Sets the foreground color to bright Red. - /// - public const string BrightForegroundRed = "\x1b[91m"; - /// - /// Sets the foreground color to bright Green. - /// - public const string BrightForegroundGreen = "\x1b[92m"; - /// - /// Sets the foreground color to bright Yellow. - /// - public const string BrightForegroundYellow = "\x1b[93m"; - /// - /// Sets the foreground color to bright Blue. - /// - public const string BrightForegroundBlue = "\x1b[94m"; - /// - /// Sets the foreground color to bright Magenta. - /// - public const string BrightForegroundMagenta = "\x1b[95m"; - /// - /// Sets the foreground color to bright Cyan. - /// - public const string BrightForegroundCyan = "\x1b[96m"; - /// - /// Sets the foreground color to bright White. - /// - public const string BrightForegroundWhite = "\x1b[97m"; - /// - /// Sets the background color to bright Black. - /// - public const string BrightBackgroundBlack = "\x1b[100m"; - /// - /// Sets the background color to bright Red. - /// - public const string BrightBackgroundRed = "\x1b[101m"; - /// - /// Sets the background color to bright Green. - /// - public const string BrightBackgroundGreen = "\x1b[102m"; - /// - /// Sets the background color to bright Yellow. - /// - public const string BrightBackgroundYellow = "\x1b[103m"; - /// - /// Sets the background color to bright Blue. - /// - public const string BrightBackgroundBlue = "\x1b[104m"; - /// - /// Sets the background color to bright Magenta. - /// - public const string BrightBackgroundMagenta = "\x1b[105m"; - /// - /// Sets the background color to bright Cyan. - /// - public const string BrightBackgroundCyan = "\x1b[106m"; - /// - /// Sets the background color to bright White. - /// - public const string BrightBackgroundWhite = "\x1b[107m"; + return new(FormattableString.Invariant($"{VirtualTerminal.Escape}[{(foreground ? 38 : 48)};2;{color.R};{color.G};{color.B}m")); + } + + private TextFormat(string value) + { + _value = value; + } + + /// + /// Returns the text formatting string contained in this instance. + /// + /// The value of the property. + public override string ToString() => Value ?? string.Empty; - /// - /// Returns the virtual terminal sequence to the foreground or background color to an RGB - /// color. - /// - /// The color to use. - /// - /// to apply the color to the background; otherwise, it's applied - /// to the background. - /// - /// A string with the virtual terminal sequence. - public static string GetExtendedColor(Color color, bool foreground = true) + /// + /// Combines two text formatting values. + /// + /// The value to combine with this one. + /// A instance that applies both the input format options. + /// + public TextFormat Combine(TextFormat other) => new(Value + other.Value); + + /// + /// Determine whether this instance and another instance have the + /// same value. + /// + /// The instance to compare to. + /// + /// if the instances are equal; otherwise, . + /// + public bool Equals(TextFormat other) => Value.Equals(other.Value, StringComparison.Ordinal); + + /// + public override bool Equals(object? obj) + { + if (obj is TextFormat format) { - return FormattableString.Invariant($"{VirtualTerminal.Escape}[{(foreground ? 38 : 48)};2;{color.R};{color.G};{color.B}m"); + return Equals(format); } + + return false; } + + /// + public override int GetHashCode() => Value.GetHashCode(); + + /// + /// Gets the text formatting string. + /// + /// + /// A string containing virtual terminal sequences, or an empty string if this structure was + /// default-initialized. + /// + public string Value => _value ?? string.Empty; + + /// + /// Combines two text formatting values. + /// + /// The first value. + /// The second value. + /// A instance that applies both the input format options. + public static TextFormat operator +(TextFormat left, TextFormat right) => left.Combine(right); + + /// + /// Determines whether this instance and another instance have the + /// same value. + /// + /// The first value. + /// The second value. + /// + /// if the instances are equal; otherwise, . + /// + public static bool operator ==(TextFormat left, TextFormat right) => left.Equals(right); + + /// + /// Determines whether this instance and another instance have a + /// different value. + /// + /// The first value. + /// The second value. + /// + /// if the instances are not equal; otherwise, . + /// + public static bool operator !=(TextFormat left, TextFormat right) => !left.Equals(right); } diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs index 86a0782f..d4e52022 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs @@ -1,192 +1,198 @@ using System; using System.Runtime.InteropServices; -namespace Ookii.CommandLine.Terminal +namespace Ookii.CommandLine.Terminal; + +/// +/// Provides helper methods for console Virtual Terminal sequences. +/// +/// +/// +/// Virtual terminal sequences are used to add color to various aspects of the usage help, +/// if enabled by the class. +/// +/// +/// +public static class VirtualTerminal { /// - /// Provides helper methods for console Virtual Terminal sequences. + /// The escape character that begins all Virtual Terminal sequences. /// + public const char Escape = '\x1b'; + + /// + /// Enables virtual terminal sequences for the console attached to the specified stream. + /// + /// 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 sequences are used to add color to various aspects of the usage help, - /// if enabled by the class. + /// Virtual terminal sequences are supported if the specified stream is not redirected, + /// and the TERM environment variable is not set to "dumb". On Windows, enabling VT + /// support has to succeed. On non-Windows platforms, VT support is assumed if the TERM + /// environment variable is defined. + /// + /// + /// For , this method does nothing and always returns + /// . /// /// - public static class VirtualTerminal + public static VirtualTerminalSupport EnableVirtualTerminalSequences(StandardStream stream) { - /// - /// The escape character that begins all Virtual Terminal sequences. - /// - public const char Escape = '\x1b'; - - /// - /// Enables virtual terminal sequences for the console attached to the specified stream. - /// - /// 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 sequences are supported if the specified stream is not redirected, - /// and the TERM environment variable is not set to "dumb". On Windows, enabling VT - /// support has to succeed. On non-Windows platforms, VT support is assumed if the TERM - /// environment variable is defined. - /// - /// - /// For , this method does nothing and always returns - /// . - /// - /// - public static VirtualTerminalSupport EnableVirtualTerminalSequences(StandardStream stream) + bool supported = stream switch { - bool supported = stream switch - { - StandardStream.Output => !Console.IsOutputRedirected, - StandardStream.Error => !Console.IsErrorRedirected, - _ => false, - }; + StandardStream.Output => !Console.IsOutputRedirected, + StandardStream.Error => !Console.IsErrorRedirected, + _ => false, + }; - if (!supported) - { - return new VirtualTerminalSupport(false); - } + if (!supported) + { + return new VirtualTerminalSupport(false); + } + + var term = Environment.GetEnvironmentVariable("TERM"); + if (term == "dumb") + { + return new VirtualTerminalSupport(false); + } - var term = Environment.GetEnvironmentVariable("TERM"); - if (term == "dumb") + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var (enabled, previousMode) = NativeMethods.EnableVirtualTerminalSequences(stream, true); + if (!enabled) { return new VirtualTerminalSupport(false); } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (previousMode is NativeMethods.ConsoleModes mode) { - var previousMode = NativeMethods.EnableVirtualTerminalSequences(stream, true); - if (previousMode == null) - { - return new VirtualTerminalSupport(false); - } - - return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), previousMode.Value); + return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), mode); } - // Support is assumed on non-Windows platforms if TERM is set. - return new VirtualTerminalSupport(term != null); + // Support was already enabled externally, so don't change the console mode on dispose. + return new VirtualTerminalSupport(true); } - /// - /// Enables color support using virtual terminal sequences for the console attached to the - /// specified stream. - /// - /// The to enable color 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. - /// - /// - /// - /// If an environment variable named "NO_COLOR" exists, this function will not enable VT - /// sequences. Otherwise, this function calls the - /// method and returns its result. - /// - /// - public static VirtualTerminalSupport EnableColor(StandardStream stream) - { - if (Environment.GetEnvironmentVariable("NO_COLOR") != null) - { - return new VirtualTerminalSupport(false); - } - - return EnableVirtualTerminalSequences(stream); - } + // Support is assumed on non-Windows platforms if TERM is set. + return new VirtualTerminalSupport(term != null); + } - // Returns the index of the character after the end of the sequence. - internal static int FindSequenceEnd(ReadOnlySpan value, ref StringSegmentType type) + /// + /// Enables color support using virtual terminal sequences for the console attached to the + /// specified stream. + /// + /// The to enable color 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. + /// + /// + /// + /// If an environment variable named "NO_COLOR" exists, this function will not enable VT + /// sequences. Otherwise, this function calls the + /// method and returns its result. + /// + /// + public static VirtualTerminalSupport EnableColor(StandardStream stream) + { + if (Environment.GetEnvironmentVariable("NO_COLOR") != null) { - if (value.Length == 0) - { - return -1; - } - - return type switch - { - StringSegmentType.PartialFormattingUnknown => value[0] switch - { - '[' => FindCsiEnd(value, ref type), - ']' => FindOscEnd(value, ref type), - // If the character after ( isn't present, we haven't found the end yet. - '(' => value.Length > 1 ? 1 : -1, - _ => 0, - }, - StringSegmentType.PartialFormattingSimple => value.Length > 0 ? 0 : -1, - StringSegmentType.PartialFormattingCsi => FindCsiEndPartial(value, ref type), - StringSegmentType.PartialFormattingOsc or StringSegmentType.PartialFormattingOscWithEscape => FindOscEndPartial(value, ref type), - _ => throw new ArgumentException("Invalid type for this operation.", nameof(type)), - }; + return new VirtualTerminalSupport(false); } - private static int FindCsiEnd(ReadOnlySpan value, ref StringSegmentType type) + return EnableVirtualTerminalSequences(stream); + } + + // Returns the index of the character after the end of the sequence. + internal static int FindSequenceEnd(ReadOnlySpan value, ref StringSegmentType type) + { + if (value.Length == 0) { - int result = FindCsiEndPartial(value.Slice(1), ref type); - return result < 0 ? result : result + 1; + return -1; } - private static int FindCsiEndPartial(ReadOnlySpan value, ref StringSegmentType type) + return type switch { - int index = 0; - foreach (var ch in value) + StringSegmentType.PartialFormattingUnknown => value[0] switch { - if (!char.IsNumber(ch) && ch != ';' && ch != ' ') - { - return index; - } + '[' => FindCsiEnd(value, ref type), + ']' => FindOscEnd(value, ref type), + // If the character after ( isn't present, we haven't found the end yet. + '(' => value.Length > 1 ? 1 : -1, + _ => 0, + }, + StringSegmentType.PartialFormattingSimple => value.Length > 0 ? 0 : -1, + StringSegmentType.PartialFormattingCsi => FindCsiEndPartial(value, ref type), + StringSegmentType.PartialFormattingOsc or StringSegmentType.PartialFormattingOscWithEscape => FindOscEndPartial(value, ref type), + _ => throw new ArgumentException("Invalid type for this operation.", nameof(type)), + }; + } - ++index; + private static int FindCsiEnd(ReadOnlySpan value, ref StringSegmentType type) + { + int result = FindCsiEndPartial(value.Slice(1), ref type); + return result < 0 ? result : result + 1; + } + + private static int FindCsiEndPartial(ReadOnlySpan value, ref StringSegmentType type) + { + int index = 0; + foreach (var ch in value) + { + if (!char.IsNumber(ch) && ch != ';' && ch != ' ') + { + return index; } - type = StringSegmentType.PartialFormattingCsi; - return -1; + ++index; } - private static int FindOscEnd(ReadOnlySpan value, ref StringSegmentType type) - { - int result = FindOscEndPartial(value.Slice(1), ref type); - return result < 0 ? result : result + 1; - } + type = StringSegmentType.PartialFormattingCsi; + return -1; + } - private static int FindOscEndPartial(ReadOnlySpan value, ref StringSegmentType type) + private static int FindOscEnd(ReadOnlySpan value, ref StringSegmentType type) + { + int result = FindOscEndPartial(value.Slice(1), ref type); + return result < 0 ? result : result + 1; + } + + private static int FindOscEndPartial(ReadOnlySpan value, ref StringSegmentType type) + { + int index = 0; + bool hasEscape = type == StringSegmentType.PartialFormattingOscWithEscape; + foreach (var ch in value) { - int index = 0; - bool hasEscape = type == StringSegmentType.PartialFormattingOscWithEscape; - foreach (var ch in value) + if (ch == 0x7) { - if (ch == 0x7) - { - return index; - } + return index; + } - if (hasEscape) + if (hasEscape) + { + if (ch == '\\') { - if (ch == '\\') - { - return index; - } - - hasEscape = false; + return index; } - if (ch == Escape) - { - hasEscape = true; - } + hasEscape = false; + } - ++index; + if (ch == Escape) + { + hasEscape = true; } - type = hasEscape ? StringSegmentType.PartialFormattingOscWithEscape : StringSegmentType.PartialFormattingOsc; - return -1; + ++index; } + + type = hasEscape ? StringSegmentType.PartialFormattingOscWithEscape : StringSegmentType.PartialFormattingOsc; + return -1; } } diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs index f1d83cb4..c4387ce1 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs @@ -1,64 +1,82 @@ using System; -namespace Ookii.CommandLine.Terminal +namespace Ookii.CommandLine.Terminal; + +/// +/// Handles the lifetime of virtual terminal support. +/// +/// +/// On Windows, this restores the terminal mode to its previous value when disposed or +/// destructed. On other platforms, this does nothing. +/// +/// +public sealed class VirtualTerminalSupport : IDisposable { + private readonly bool _supported; + private IntPtr _handle; + private readonly NativeMethods.ConsoleModes _previousMode; + + internal VirtualTerminalSupport(bool supported) + { + _supported = supported; + GC.SuppressFinalize(this); + } + + internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previousMode) + { + _supported = true; + _handle = handle; + _previousMode = previousMode; + } + /// - /// Handles the lifetime of virtual terminal support. + /// Cleans up resources for the class. /// /// - /// On Windows, this restores the terminal mode to its previous value when disposed or - /// destructed. On other platforms, this does nothing. + /// + /// This method will disable VT support on Windows if it was enabled by the call to + /// or + /// that + /// created this instance. + /// /// - public sealed class VirtualTerminalSupport : IDisposable + ~VirtualTerminalSupport() { - private readonly bool _supported; - private IntPtr _handle; - private readonly NativeMethods.ConsoleModes _previousMode; - - internal VirtualTerminalSupport(bool supported) - { - _supported = supported; - GC.SuppressFinalize(this); - } - - internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previousMode) - { - _supported = true; - _handle = handle; - _previousMode = previousMode; - } - - /// - /// Cleans up resources for the class. - /// - ~VirtualTerminalSupport() - { - ResetConsoleMode(); - } + ResetConsoleMode(); + } - /// - /// Gets a value that indicates whether virtual terminal sequences are supported. - /// - /// - /// if virtual terminal sequences are supported; otherwise, - /// . - /// - public bool IsSupported => _supported; + /// + /// Gets a value that indicates whether virtual terminal sequences are supported. + /// + /// + /// if virtual terminal sequences are supported; otherwise, + /// . + /// + public bool IsSupported => _supported; - /// - public void Dispose() - { - ResetConsoleMode(); - GC.SuppressFinalize(this); - } + /// + /// Cleans up resources for the class. + /// + /// + /// + /// This method will disable VT support on Windows if it was enabled by the call to + /// or + /// that + /// created this instance. + /// + /// + public void Dispose() + { + ResetConsoleMode(); + GC.SuppressFinalize(this); + } - private void ResetConsoleMode() + private void ResetConsoleMode() + { + if (_handle != IntPtr.Zero) { - if (_handle != IntPtr.Zero) - { - NativeMethods.SetConsoleMode(_handle, _previousMode); - _handle = IntPtr.Zero; - } + NativeMethods.SetConsoleMode(_handle, _previousMode); + _handle = IntPtr.Zero; } } } diff --git a/src/Ookii.CommandLine/TypeConverterBase.cs b/src/Ookii.CommandLine/TypeConverterBase.cs deleted file mode 100644 index 92450790..00000000 --- a/src/Ookii.CommandLine/TypeConverterBase.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; - -namespace Ookii.CommandLine -{ - /// - /// Base class to help with implementing a that can convert to/from - /// a . - /// - /// The type of object that can be converted to/from a string. - /// - /// - /// This class handles checking whether the source or destination type is a string, and calls - /// strongly typed conversion methods that inheritors can implement. - /// - /// - /// For the method, - /// it relies on the fact that the base implementation already - /// returns for the type. - /// - /// - public abstract class TypeConverterBase : TypeConverter - { - /// - /// - /// if the is ; - /// otherwise, . - /// - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); - } - - /// - /// - /// - /// If the is an instance of the type, this - /// method calls . - /// Otherwise, it calls the base - /// method. - /// - /// - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - if (value is string stringValue) - { - return Convert(context, culture, stringValue); - } - - return base.ConvertFrom(context, culture, value); - } - - /// - /// - /// - /// If the is , this method will - /// call the method. Otherwise, - /// the base - /// method is called. - /// - /// - /// If the method returns - /// , conversion falls back to the base - /// method, which uses to convert to a . - /// - /// - public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) - { - if (value is T typedValue && destinationType == typeof(string)) - { - var converted = Convert(context, culture, typedValue); - if (converted != null) - { - return converted; - } - } - - return base.ConvertTo(context, culture, value, destinationType); - } - - /// - /// When implemented in a derived class, converts from a string to the type of this - /// converter. - /// - /// An that provides format context. - /// A to use for the conversion. - /// The value to convert. - /// The converted object. - protected abstract T? Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value); - - /// - /// Converts the type of this converter to a string. - /// - /// An that provides format context. - /// A to use for the conversion. - /// The object to convert. - /// - /// A string representing the object, or if the caller should fall - /// back to using . The base class implementation always returns - /// . - /// - protected virtual string? Convert(ITypeDescriptorContext? context, CultureInfo? culture, T value) => null; - } -} diff --git a/src/Ookii.CommandLine/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index e52f03dc..5166375d 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -1,140 +1,172 @@ -// Copyright (c) Sven Groot (Ookii.org) +using Ookii.CommandLine.Conversion; using System; -using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +static class TypeHelper { - static class TypeHelper + private const string ParseMethodName = "Parse"; + + public static Type? FindGenericInterface( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + this Type type, Type interfaceType) { - private const string ParseMethodName = "Parse"; + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } - public static Type? FindGenericInterface(this Type type, Type interfaceType) + if (interfaceType == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + throw new ArgumentNullException(nameof(interfaceType)); + } - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } + if (!(interfaceType.IsInterface && interfaceType.IsGenericTypeDefinition)) + { + throw new ArgumentException(Properties.Resources.TypeNotGenericDefinition, nameof(interfaceType)); + } - if (!(interfaceType.IsInterface && interfaceType.IsGenericTypeDefinition)) - { - throw new ArgumentException(Properties.Resources.TypeNotGenericDefinition, nameof(interfaceType)); - } + if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == interfaceType) + { + return type; + } - if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == interfaceType) - { - return type; - } + return type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); + } - return type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); + public static bool ImplementsInterface( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + this Type type, Type interfaceType) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); } - public static bool ImplementsInterface(this Type type, Type interfaceType) + if (interfaceType == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + throw new ArgumentNullException(nameof(interfaceType)); + } - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } + return type.GetInterfaces().Any(i => i == interfaceType); + } - return type.GetInterfaces().Any(i => i == interfaceType); + public static object? CreateInstance( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + this Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); } - public static object? CreateInstance(this Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + return Activator.CreateInstance(type); + } - return Activator.CreateInstance(type); +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] +#endif + public static ArgumentConverter GetStringConverter(this Type type, Type? converterType) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); } - public static object? CreateInstance(this Type type, params object?[]? args) + var converter = (ArgumentConverter?)converterType?.CreateInstance(); + if (converter != null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - return Activator.CreateInstance(type, args); + return converter; } - public static TypeConverter GetStringConverter(this Type type, Type? converterType) + if (converterType == null) { - if (type == null) + var underlyingType = type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; + converter = GetDefaultConverter(underlyingType); + if (converter != null) { - throw new ArgumentNullException(nameof(type)); + return type.IsNullableValueType() + ? new NullableConverter(converter) + : converter; } + } - var converter = (TypeConverter?)converterType?.CreateInstance() ?? TypeDescriptor.GetConverter(type); - if (converter != null && converter.CanConvertFrom(typeof(string))) - { - return converter; - } + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoArgumentConverterFormat, type)); + } - if (converterType == null) - { - var underlyingType = type.GetUnderlyingType(); - converter = GetDefaultConverter(underlyingType); - if (converter != null) - { - return type.IsNullableValueType() - ? new NullableConverterWrapper(underlyingType, converter) - : converter; - } - } + public static bool IsNullableValueType(this Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + public static Type GetUnderlyingType(this Type type) + => type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoTypeConverterFormat, type)); + private static ArgumentConverter? GetDefaultConverter( +#if NET7_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] +#elif NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + this Type type) + { + if (type == typeof(string)) + { + return StringConverter.Instance; } - public static bool IsNullableValueType(this Type type) + if (type == typeof(bool)) { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + return BooleanConverter.Instance; } - public static Type GetUnderlyingType(this Type type) - => type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; + if (type.IsEnum) + { + return new EnumConverter(type); + } - private static TypeConverter? GetDefaultConverter(this Type type) +#if NET7_0_OR_GREATER + if (type.FindGenericInterface(typeof(ISpanParsable<>)) != null) { - // If no explicit converter and the default one can't converter from string, see if - // there's a Parse method we can use. - var method = type.GetMethod(ParseMethodName, BindingFlags.Static | BindingFlags.Public, - null, new[] { typeof(string), typeof(CultureInfo) }, null); + return (ArgumentConverter?)Activator.CreateInstance(typeof(SpanParsableConverter<>).MakeGenericType(type)); + } +#endif - if (method != null && method.ReturnType == type) - { - return new ParseTypeConverter(method, true); - } + // If no explicit converter and the default one can't converter from string, see if + // there's a Parse method we can use. + var method = type.GetMethod(ParseMethodName, BindingFlags.Static | BindingFlags.Public, + null, new[] { typeof(string), typeof(CultureInfo) }, null); - // Check for Parse without a culture arguments. - method = type.GetMethod(ParseMethodName, BindingFlags.Static | BindingFlags.Public, null, - new[] { typeof(string) }, null); + if (method != null && method.ReturnType == type) + { + return new ParseConverter(method, true); + } - if (method != null && method.ReturnType == type) - { - return new ParseTypeConverter(method, false); - } + // Check for Parse without a culture arguments. + method = type.GetMethod(ParseMethodName, BindingFlags.Static | BindingFlags.Public, null, + new[] { typeof(string) }, null); - // Check for a constructor with a string argument. - if (type.GetConstructor(new[] { typeof(string) }) != null) - { - return new ConstructorTypeConverter(type); - } + if (method != null && method.ReturnType == type) + { + return new ParseConverter(method, false); + } - return null; + // Check for a constructor with a string argument. + if (type.GetConstructor(new[] { typeof(string) }) != null) + { + return new ConstructorConverter(type); } + + return null; } } diff --git a/src/Ookii.CommandLine/UsageHelpRequest.cs b/src/Ookii.CommandLine/UsageHelpRequest.cs index 03a722cb..5fdc8cf5 100644 --- a/src/Ookii.CommandLine/UsageHelpRequest.cs +++ b/src/Ookii.CommandLine/UsageHelpRequest.cs @@ -1,24 +1,23 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates if and how usage is shown if an error occurred parsing the command line. +/// +/// +public enum UsageHelpRequest { /// - /// Indicates if and how usage is shown if an error occurred parsing the command line. + /// Only the usage syntax is shown; the argument descriptions are not. In addition, the + /// message is shown. /// - /// - public enum UsageHelpRequest - { - /// - /// Full usage help is shown, including the argument descriptions. - /// - Full, - /// - /// Only the usage syntax is shown; the argument descriptions are not. In addition, the - /// message is shown. - /// - SyntaxOnly, - /// - /// No usage help is shown. Instead, the - /// message is shown. - /// - None - } + SyntaxOnly, + /// + /// Full usage help is shown, including the argument descriptions. + /// + Full, + /// + /// No usage help is shown. Instead, the + /// message is shown. + /// + None } diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index cf6a3da7..02e4e6cb 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -4,2117 +4,2261 @@ using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Creates usage help for the class and the +/// class. +/// +/// +/// +/// You can derive from this class to override the formatting of various aspects of the usage +/// help. Set the property to specify a custom instance. +/// +/// +/// Depending on what methods you override, you can change small parts of the formatting, or +/// completely change how usage looks. Certain methods may not be called if you override the +/// methods that call them. +/// +/// +/// This class has a number of properties that customize the usage help for the base +/// implementation of this class. It is not guaranteed that a derived class will respect +/// these properties. +/// +/// +/// +public class UsageWriter { + #region Nested types + + /// + /// Indicates the type of operation in progress. + /// + /// + protected enum Operation + { + /// + /// No operation is in progress. + /// + None, + /// + /// A call to is in progress. + /// + ParserUsage, + /// + /// A call to is in progress. + /// + CommandListUsage, + } + + #endregion + + /// + /// The default indentation for the usage syntax. + /// + /// + /// The default indentation, which is three characters. + /// + /// + public const int DefaultSyntaxIndent = 3; + + /// + /// The default indentation for the argument descriptions. + /// + /// + /// The default indentation, which is eight characters. + /// + /// + public const int DefaultArgumentDescriptionIndent = 8; + + /// + /// The default indentation for the application description. + /// + /// + /// The default indentation, which is zero. + /// + /// + public const int DefaultApplicationDescriptionIndent = 0; + + /// + /// Gets the default value for the property. + /// + public const int DefaultCommandDescriptionIndent = 8; + + // Don't apply indentation to console output if the line width is less than this. + private const int MinimumLineWidthForIndent = 30; + + private const char OptionalStart = '['; + private const char OptionalEnd = ']'; + + private LineWrappingTextWriter? _writer; + private bool? _useColor; + private CommandLineParser? _parser; + private CommandManager? _commandManager; + private string? _executableName; + private string? _defaultExecutableName; + private bool _includeExecutableExtension; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A instance to write usage to, or + /// to write to the standard output stream. + /// + /// + /// to enable color output using virtual terminal sequences; + /// to disable it; or, to automatically + /// enable it if is using the + /// method. + /// + /// + /// + /// If the parameter is , output is + /// written to a for the standard output stream, + /// wrapping at the console's window width. If the stream is redirected, output may still + /// be wrapped, depending on the value returned by . + /// + /// + public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) + { + _writer = writer; + _useColor = useColor; + } + + /// + /// Gets or sets a value indicating whether the value of the property + /// is written before the syntax. + /// + /// + /// if the value of the property + /// is written before the syntax; otherwise, . The default value is . + /// + public bool IncludeApplicationDescription { get; set; } = true; + + /// + /// The indentation to use for the application description. + /// + /// + /// The indentation. The default value is the value of the + /// constant. + /// + /// + /// + /// This property is only used if the property + /// is . + /// + /// + /// This also applies to the command description when showing usage help for a subcommand. + /// + /// + /// + public int ApplicationDescriptionIndent { get; set; } = DefaultApplicationDescriptionIndent; + + /// + /// Gets or sets the application executable name used in the usage help. + /// + /// + /// The application executable name. + /// + /// + /// + /// Set this property to to use the default value, determined by + /// calling the + /// method. + /// + /// + /// +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + [AllowNull] +#endif + public virtual string ExecutableName + { + get => _executableName ?? (_defaultExecutableName ??= CommandLineParser.GetExecutableName(IncludeExecutableExtension)); + set => _executableName = value; + } + + /// + /// Gets or sets a value that indicates whether the usage syntax should include the file + /// name extension of the application's executable. + /// + /// + /// if the extension should be included; otherwise, . + /// The default value is . + /// + /// + /// + /// If the property is , the executable + /// name is determined by calling , + /// passing the value of this property as the argument. + /// + /// + /// This property is not used if the property is not + /// . + /// + /// + public bool IncludeExecutableExtension + { + get => _includeExecutableExtension; + set + { + _includeExecutableExtension = value; + _defaultExecutableName = null; + } + } + + /// + /// Gets or sets the color applied by the method. + /// + /// + /// The virtual terminal sequence for a color. The default value is + /// . + /// + /// + /// + /// The color will only be used if the property is + /// . + /// + /// + /// The portion of the string that has color will end with the value of the + /// property. + /// + /// + /// With the base implementation, only the "Usage:" portion of the string has color; the + /// executable name does not. + /// + /// + public TextFormat UsagePrefixColor { get; set; } = TextFormat.ForegroundCyan; + + /// + /// Gets or sets the number of characters by which to indent all except the first line of the + /// command line syntax of the usage help. + /// + /// + /// The number of characters by which to indent the usage syntax. The default value is the + /// value of the constant. + /// + /// + /// + /// The command line syntax is a single line that consists of the usage prefix written + /// by followed by the syntax of all + /// the arguments. This indentation is used when that line exceeds the maximum line + /// length. + /// + /// + /// This value is used by the base implementation of the + /// class, unless the property is . + /// + /// + public int SyntaxIndent { get; set; } = DefaultSyntaxIndent; + + /// + /// Gets or sets a value that indicates whether the usage syntax should use short names + /// for arguments that have one. + /// + /// + /// to use short names for arguments that have one; otherwise, + /// to use the long name. The default value is . + /// + /// + /// + /// This property is only used when the + /// property is . + /// + /// + public bool UseShortNamesForSyntax { get; set; } + + /// + /// Gets or sets a value that indicates whether to list only positional arguments in the + /// usage syntax. + /// + /// + /// to abbreviate the syntax; otherwise, . + /// The default value is . + /// + /// + /// + /// Abbreviated usage syntax only lists the positional arguments explicitly. After that, + /// if there are any more arguments, it will just print the value from the + /// method. The user will have to refer + /// to the description list to see the remaining possible + /// arguments. + /// + /// + /// Use this if your application has a very large number of arguments. + /// + /// + public bool UseAbbreviatedSyntax { get; set; } + + /// + /// Gets or sets the number of characters by which to indent all but the first line of each + /// argument's description, if the property is + /// . + /// + /// + /// The number of characters by which to indent the argument descriptions. The default + /// value is the value of the constant. + /// + /// + /// + /// This value is used by the base implementation of the + /// method, unless the property is . + /// + /// + public int ArgumentDescriptionIndent { get; set; } = DefaultArgumentDescriptionIndent; + + /// + /// Gets or sets a value that indicates which arguments should be included in the list of + /// argument descriptions. + /// + /// + /// One of the values of the enumeration. The default + /// value is . + /// + public DescriptionListFilterMode ArgumentDescriptionListFilter { get; set; } + + /// + /// Gets or sets a value that indicates the order of the arguments in the list of argument + /// descriptions. + /// + /// + /// One of the values of the enumeration. The default + /// value is . + /// + public DescriptionListSortMode ArgumentDescriptionListOrder { get; set; } + + /// + /// Gets or sets the color applied by the method. + /// + /// + /// The virtual terminal sequence for a color. The default value is + /// . + /// + /// + /// + /// The color will only be used if the property is + /// . + /// + /// + /// The portion of the string that has color will end with the value of the + /// property. + /// + /// + /// With the default format, only the argument name, value description and aliases + /// portion of the string has color; the actual argument description does not. + /// + /// + public TextFormat ArgumentDescriptionColor { get; set; } = TextFormat.ForegroundGreen; + + /// + /// Gets or sets a value indicating whether white space, rather than the first element of the + /// property, is used to + /// separate arguments and their values in the command line syntax. + /// + /// + /// if the command line syntax uses a white space value separator; + /// if it uses the first element of the + /// property. The default value is . + /// + /// + /// + /// If this property is , an argument would be formatted in the command + /// line syntax as "-Name <Value>" (using default formatting), with a white space + /// character separating the argument name and value description. If this property is + /// , it would be formatted as "-Name:<Value>", using a colon as the + /// separator (when using the default separators). + /// + /// + /// The command line syntax will only use a white space character as the value separator if + /// both the + /// property and the property are + /// . + /// + /// + public bool UseWhiteSpaceValueSeparator { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the alias or aliases of an argument should be included in the argument description.. + /// + /// + /// if the alias(es) should be included in the description; + /// otherwise, . The default value is . + /// + /// + /// + /// For arguments that do not have any aliases, this property has no effect. + /// + /// + public bool IncludeAliasInDescription { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the default value of an argument should be included in the argument description. + /// + /// + /// if the default value should be included in the description; + /// otherwise, . The default value is . + /// + /// + /// + /// For arguments with a default value of , this property has no effect. + /// + /// + /// To exclude the default value for a particular argument only, use the + /// + /// property. + /// + /// + public bool IncludeDefaultValueInDescription { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the + /// attributes of an argument should be included in the argument description. + /// + /// + /// if the validator descriptions should be included in; otherwise, + /// . The default value is . + /// + /// + /// + /// For arguments with no validators, or validators with no usage help, this property + /// has no effect. + /// + /// + /// For validators derived from the class, + /// you can use the + /// property to exclude the help text for individual validators. + /// + /// + public bool IncludeValidatorsInDescription { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the + /// method will write a blank line between arguments in the description list. + /// + /// + /// to write a blank line; otherwise, . The + /// default value is . + /// + public bool BlankLineAfterDescription { get; set; } = true; + + /// + /// Gets or sets the virtual terminal sequence used to undo a color change that was applied + /// to a usage help element. + /// + /// + /// The virtual terminal sequence used to reset color. The default value is + /// . + /// + /// + /// + /// This property will only be used if the property is + /// . + /// + /// + public TextFormat ColorReset { get; set; } = TextFormat.Default; + + /// + /// Gets or sets the name of the subcommand. + /// + /// + /// The name of the subcommand, or if the current parser is not for + /// a subcommand. + /// + /// + /// + /// This property is set by the class before writing usage + /// help for a subcommand. + /// + /// + /// When nested subcommands are used with the class, this may be + /// several subcommand names separated by spaces. + /// + /// + public string? CommandName { 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; + + /// + /// Gets or sets the color applied by the base implementation of the + /// method. + /// + /// + /// The virtual terminal sequence for a color. The default value is + /// . + /// + /// + /// + /// The color will only be used if the property is + /// . + /// + /// + /// The portion of the string that has color will end with the . + /// + /// + /// With the default implementation, only the command name portion of the string has color; + /// the application name does not. + /// + /// + public TextFormat CommandDescriptionColor { get; set; } = TextFormat.ForegroundGreen; + + /// + /// Gets or sets the number of characters by which to indent the all but the first line of command descriptions. + /// + /// + /// The number of characters by which to indent the all but the first line of command descriptions. The default value is 8. + /// + /// + /// + /// This value is used by the base implementation of the + /// method, unless the property is . + /// + /// + public int CommandDescriptionIndent { get; set; } = DefaultCommandDescriptionIndent; + + /// + /// Gets or sets a value indicating whether the + /// method will write a blank line between commands in the command list. + /// + /// + /// to write a blank line; otherwise, . The + /// default value is . + /// + public bool BlankLineAfterCommandDescription { get; set; } = true; + + /// + /// Gets or sets a value that indicates whether a message is shown at the bottom of the + /// command list that instructs the user how to get help for individual commands. + /// + /// + /// to show the instruction if all commands have the default help + /// argument; to always show the instruction; otherwise, + /// . The default value is . + /// + /// + /// + /// If this property is , the instruction will be shown under the + /// following conditions: + /// + /// + /// + /// + /// The property is + /// or . + /// + /// + /// + /// + /// For every command with a attribute, the + /// property is + /// . + /// + /// + /// + /// + /// No command uses the interface (this includes + /// commands that derive from the class). + /// + /// + /// + /// + /// No command specifies custom values for the + /// and + /// properties. + /// + /// + /// + /// + /// Every command uses the same values for the + /// and properties. + /// + /// + /// + /// + /// If set to , the message is shown even if not all commands meet these + /// restrictions. + /// + /// + /// To customize the message, override the method. + /// + /// + public bool? IncludeCommandHelpInstruction { get; set; } + + /// + /// Gets or sets a value that indicates whether to show the application description before + /// the command list in the usage help. + /// + /// + /// to show the description; otherwise, . The + /// default value is . + /// + /// + /// + /// The description to show is taken from the + /// of the first assembly passed to the class. If the + /// assembly has no description, nothing is written. + /// + /// + /// If the property is not , + /// and the specified type has a , that description is + /// used instead. + /// + /// + /// To use a custom description, set this property to , and override + /// the method. + /// + /// + public bool IncludeApplicationDescriptionBeforeCommandList { get; set; } + + /// + /// Gets or sets a value that indicates whether to show a command's aliases as part of the + /// command list usage help. + /// + /// + /// to show the command's aliases; otherwise, . + /// The default value is . + /// + public bool IncludeCommandAliasInCommandList { get; set; } = true; + + /// + /// Gets the to which the usage should be written. + /// + /// + /// The passed to the + /// constructor, or an instance created by the + /// or + /// function. + /// + /// + /// No was passed to the constructor, and a + /// operation is not in progress. + /// + protected LineWrappingTextWriter Writer + => _writer ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + + /// + /// Gets the that usage is being written for. + /// + /// + /// A operation is not in progress. + /// + protected CommandLineParser Parser + => _parser ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + + /// + /// Gets the that usage is being written for. + /// + /// + /// A operation is not in progress. + /// + protected CommandManager CommandManager + => _commandManager ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + + /// + /// Indicates what operation is currently in progress. + /// + /// + /// One of the values of the enumeration. + /// + /// + /// + /// If this property is not , the + /// property will throw an exception. + /// + /// + /// If this property is not , the + /// property will throw an exception. + /// + /// + /// If this property is , the + /// property may throw an exception. + /// + /// + protected Operation OperationInProgress + { + get + { + if (_parser != null) + { + return Operation.ParserUsage; + } + else if (_commandManager != null) + { + return Operation.CommandListUsage; + } + + return Operation.None; + } + } + + /// + /// Gets a value that indicates whether indentation should be enabled in the output. + /// + /// + /// if the property's maximum line length is + /// unlimited or greater than 30; otherwise, . + /// + /// + /// No was passed to the constructor, and a + /// operation is not in progress. + /// + protected virtual bool ShouldIndent => Writer.MaximumLineLength is 0 or >= MinimumLineWidthForIndent; + + /// + /// Gets the separator used between multiple consecutive argument names, command names, and + /// aliases in the usage help. + /// + /// + /// The string ", ". + /// + protected virtual string NameSeparator => ", "; + + /// + /// Creates usage help for the specified parser. + /// + /// The . + /// The parts of usage to write. + /// + /// is . + /// + /// + /// + /// If no writer was passed to the + /// constructor, this method will create a for the + /// standard output stream. If color usage wasn't explicitly enabled, it will be enabled + /// if the output supports it according to . + /// + /// + /// This method calls the method to create the usage help + /// text. + /// + /// + public void WriteParserUsage(CommandLineParser parser, UsageHelpRequest request = UsageHelpRequest.Full) + { + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + WriteUsageInternal(request); + } + /// - /// Creates usage help for the class and the - /// class. + /// Creates usage help for the specified command manager. /// + /// The + /// + /// is . + /// /// /// - /// You can derive from this class to override the formatting of various aspects of the usage - /// help. Set the property to specify a custom instance. + /// The usage help will contain a list of all available commands. /// /// - /// Depending on what methods you override, you can change small parts of the formatting, or - /// completely change how usage looks. Certain methods may not be called if you override the - /// methods that call them. + /// If no writer was passed to the + /// constructor, this method will create a for the + /// standard output stream. If color usage wasn't explicitly enabled, it will be enabled + /// if the output supports it according to . /// /// - /// This class has a number of properties that customize the usage help for the base - /// implementation of this class. It is not guaranteed that a derived class will respect - /// these properties. + /// This method calls the method to create the + /// usage help text. /// /// - public class UsageWriter + public void WriteCommandListUsage(CommandManager manager) { - #region Nested types + _commandManager = manager ?? throw new ArgumentNullException(nameof(manager)); + WriteUsageInternal(); + } - /// - /// Indicates the type of operation in progress. - /// - /// - protected enum Operation + /// + /// Returns a string with usage help for the specified parser. + /// + /// A string containing the usage help. + /// The . + /// The parts of usage to write. + /// + /// The length at which to white-space wrap lines in the output, or 0 to disable wrapping. + /// + /// + /// is . + /// + /// + /// + /// This method ignores the writer passed to the + /// constructor, and will use the + /// method instead, and returns the resulting string. If color support was not explicitly + /// enabled, it will be disabled. + /// + /// + /// This method calls the method to create the usage help + /// text. + /// + /// + public string GetUsage(CommandLineParser parser, UsageHelpRequest request = UsageHelpRequest.Full, int maximumLineLength = 0) + { + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + return GetUsageInternal(maximumLineLength, request); + } + + /// + /// Returns a string with usage help for the specified command manager. + /// + /// A string containing the usage help. + /// The . + /// + /// The length at which to white-space wrap lines in the output, or 0 to disable wrapping. + /// + /// + /// is . + /// + /// + /// + /// The usage help will contain a list of all available commands. + /// + /// + /// This method ignores the writer passed to the + /// constructor, and will use the + /// method instead, and returns the resulting string. If color support was not explicitly + /// enabled, it will be disabled. + /// + /// + /// This method calls the method to create the + /// usage help text. + /// + /// + public string GetCommandListUsage(CommandManager manager, int maximumLineLength = 0) + { + _commandManager = manager ?? throw new ArgumentNullException(nameof(manager)); + return GetUsageInternal(maximumLineLength); + } + + #region CommandLineParser usage + + /// + /// Creates the usage help for a instance. + /// + /// The parts of usage to write. + /// + /// + /// This is the primary method used to generate usage help for the + /// class. It calls into the various other methods of this class, so overriding this + /// method should not typically be necessary unless you wish to deviate from the order + /// in which usage elements are written. + /// + /// + /// The base implementation writes the application description, followed by the usage + /// syntax, followed by the class validator help messages, followed by a list of argument + /// descriptions. Which elements are included exactly can be influenced by the + /// parameter and the properties of this class. + /// + /// + protected virtual void WriteParserUsageCore(UsageHelpRequest request) + { + if (request == UsageHelpRequest.None) { - /// - /// No operation is in progress. - /// - None, - /// - /// A call to is in progress. - /// - ParserUsage, - /// - /// A call to is in progress. - /// - CommandListUsage, + WriteMoreInfoMessage(); + return; } - #endregion + if (request == UsageHelpRequest.Full && IncludeApplicationDescription && !string.IsNullOrEmpty(Parser.Description)) + { + WriteApplicationDescription(Parser.Description); + } - /// - /// The default indentation for the usage syntax. - /// - /// - /// The default indentation, which is three characters. - /// - /// - public const int DefaultSyntaxIndent = 3; + WriteParserUsageSyntax(); + if (request == UsageHelpRequest.Full) + { + if (IncludeValidatorsInDescription) + { + WriteClassValidators(); + } - /// - /// The default indentation for the argument descriptions for the - /// mode. - /// - /// - /// The default indentation, which is eight characters. - /// - /// - public const int DefaultArgumentDescriptionIndent = 8; + WriteArgumentDescriptions(); + Writer.Indent = 0; + } + else + { + Writer.Indent = 0; + WriteMoreInfoMessage(); + } + } - /// - /// The default indentation for the application description. - /// - /// - /// The default indentation, which is zero. - /// - /// - public const int DefaultApplicationDescriptionIndent = 0; + /// + /// Writes the application description, or command description in case of a subcommand. + /// + /// The description. + /// + /// + /// This method is called by the base implementation of the + /// method if the command has a description and the + /// property is . + /// + /// + /// This method is called by the base implementation of the + /// method if the assembly has a description and the + /// property is . + /// + /// + protected virtual void WriteApplicationDescription(string description) + { + SetIndent(ApplicationDescriptionIndent); + WriteLine(description); + WriteLine(); + } - /// - /// Gets the default value for the property. - /// - public const int DefaultCommandDescriptionIndent = 8; + /// + /// Writes the usage syntax for the application or subcommand. + /// + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteParserUsageSyntax() + { + Writer.ResetIndent(); + SetIndent(SyntaxIndent); + + WriteUsageSyntaxPrefix(); + foreach (CommandLineArgument argument in GetArgumentsInUsageOrder()) + { + Write(" "); + if (UseAbbreviatedSyntax && argument.Position == null) + { + WriteAbbreviatedRemainingArguments(); + break; + } - // Don't apply indentation to console output if the line width is less than this. - private const int MinimumLineWidthForIndent = 30; + if (argument.IsRequired) + { + WriteArgumentSyntax(argument); + } + else + { + WriteOptionalArgumentSyntax(argument); + } + } - private const char OptionalStart = '['; - private const char OptionalEnd = ']'; + WriteUsageSyntaxSuffix(); + WriteLine(); // End syntax line + WriteLine(); // Blank line + } - private LineWrappingTextWriter? _writer; - private bool? _useColor; - private CommandLineParser? _parser; - private CommandManager? _commandManager; - private string? _executableName; - private string? _defaultExecutableName; - private bool _includeExecutableExtension; + /// + /// Gets the arguments in the order they will be shown in the usage syntax. + /// + /// A list of all arguments in usage order. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + /// The base implementation first returns positional arguments in the specified order, + /// then required non-positional arguments in alphabetical order, then the remaining + /// arguments in alphabetical order. + /// + /// + /// Arguments that are hidden are excluded from the list. + /// + /// + protected virtual IEnumerable GetArgumentsInUsageOrder() => Parser.Arguments.Where(a => !a.IsHidden); - /// - /// Initializes a new instance of the class. - /// - /// - /// A instance to write usage to, or - /// to write to the standard output stream. - /// - /// - /// to enable color output using virtual terminal sequences; - /// to disable it; or, to automatically - /// enable it if is using the - /// method. - /// - /// - /// - /// If the parameter is , output is - /// written to a for the standard output stream, - /// wrapping at the console's window width. If the stream is redirected, output may still - /// be wrapped, depending on the value returned by . - /// - /// - public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) + /// + /// Write the prefix for the usage syntax, including the executable name and, for + /// subcommands, the command name. + /// + /// + /// + /// The base implementation returns a string like "Usage: executable" or "Usage: executable + /// command". If color is enabled, part of the string will be colored using the + /// property. + /// + /// + /// An implementation of this method should typically include the value of the + /// property, and the value of the + /// property if it is not . + /// + /// + /// This method is called by the base implementation of the + /// method and the method. + /// + /// + protected virtual void WriteUsageSyntaxPrefix() + { + WriteColor(UsagePrefixColor); + Write(Resources.DefaultUsagePrefix); + ResetColor(); + Write(' '); + Write(ExecutableName); + if (CommandName != null) { - _writer = writer; - _useColor = useColor; + Write(' '); + Write(CommandName); } + } - /// - /// Gets or sets a value indicating whether the value of the property - /// is written before the syntax. - /// - /// - /// if the value of the property - /// is written before the syntax; otherwise, . The default value is . - /// - public bool IncludeApplicationDescription { get; set; } = true; + /// + /// Write the suffix for the usage syntax. + /// + /// + /// + /// The base implementation does nothing for parser usage, and writes a string like + /// " <command> [arguments]" for command manager usage. + /// + /// + /// This method is called by the base implementation of the + /// method and the method. + /// + /// + protected virtual void WriteUsageSyntaxSuffix() + { + if (OperationInProgress == Operation.CommandListUsage) + { + WriteLine(Resources.DefaultCommandUsageSuffix); + } + } - /// - /// The indentation to use for the application description. - /// - /// - /// The indentation. The default value is the value of the - /// constant. - /// - /// - /// - /// This property is only used if the property - /// is . - /// - /// - /// This also applies to the command description when showing usage help for a subcommand. - /// - /// - /// - public int ApplicationDescriptionIndent { get; set; } = DefaultApplicationDescriptionIndent; + /// + /// Writes the syntax for a single optional argument. + /// + /// The argument. + /// + /// + /// The base implementation surrounds the result of the + /// method in square brackets. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteOptionalArgumentSyntax(CommandLineArgument argument) + { + Write(OptionalStart); + WriteArgumentSyntax(argument); + Write(OptionalEnd); + } + + /// + /// Writes the syntax for a single argument. + /// + /// The argument. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteArgumentSyntax(CommandLineArgument argument) + { + string argumentName; + if (argument.HasShortName && UseShortNamesForSyntax) + { + argumentName = argument.ShortName.ToString(); + } + else + { + argumentName = argument.ArgumentName; + } + + var prefix = argument.Parser.Mode != ParsingMode.LongShort || (argument.HasShortName && (UseShortNamesForSyntax || !argument.HasLongName)) + ? argument.Parser.ArgumentNamePrefixes[0] + : argument.Parser.LongArgumentNamePrefix!; + + char? separator = argument.Parser.AllowWhiteSpaceValueSeparator && UseWhiteSpaceValueSeparator + ? null + : argument.Parser.NameValueSeparators[0]; + + if (argument.Position == null) + { + WriteArgumentName(argumentName, prefix); + } + else + { + WritePositionalArgumentName(argumentName, prefix, separator); + } + + if (!argument.IsSwitch) + { + // Otherwise, the separator was included in the argument name. + if (argument.Position == null || separator == null) + { + Write(separator ?? ' '); + } + + WriteValueDescription(argument.ValueDescription); + } + + if (argument.MultiValueInfo != null) + { + WriteMultiValueSuffix(); + } + } + + /// + /// Writes the name of an argument. + /// + /// The name of the argument. + /// + /// The argument name prefix; if using , this may vary + /// depending on whether the name is a short or long name. + /// + /// + /// + /// The default implementation returns the prefix followed by the name, e.g. "-Name". + /// + /// + /// This method is called by the base implementation of the + /// method and the method. + /// + /// + protected virtual void WriteArgumentName(string argumentName, string prefix) + { + Write(prefix); + Write(argumentName); + } + + /// + /// Writes the name of a positional argument. + /// + /// The name of the argument. + /// + /// The argument name prefix; if using , this may vary + /// depending on whether the name is a short or long name. + /// + /// + /// The argument name/value separator, or if the + /// property and the property + /// are both . + /// + /// + /// + /// The default implementation surrounds the value written by the + /// method, as well as the if not , + /// with square brackets. For example, "[-Name]" or "[-Name:]", to indicate the name + /// itself is optional. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WritePositionalArgumentName(string argumentName, string prefix, char? separator) + { + Write(OptionalStart); + WriteArgumentName(argumentName, prefix); + if (separator is char separatorValue) + { + Write(separatorValue); + } + + Write(OptionalEnd); + } + + /// + /// Writes the value description of an argument. + /// + /// The value description. + /// + /// + /// The base implementation returns the value description surrounded by angle brackets. + /// For example, "<String>". + /// + /// + /// This method is called by the base implementation of the + /// method for arguments that are not switch arguments. + /// + /// + protected virtual void WriteValueDescription(string valueDescription) + => Write($"<{valueDescription}>"); + + /// + /// Writes the string used to indicate there are more arguments if the usage syntax was + /// abbreviated. + /// + /// + /// + /// The default implementation returns a string like "[arguments]". + /// + /// + /// This method is called by the base implementation of the + /// method if the property is . + /// + /// + protected virtual void WriteAbbreviatedRemainingArguments() + => Write(Resources.DefaultAbbreviatedRemainingArguments); + + /// + /// Writes a suffix that indicates an argument is a multi-value argument. + /// + /// + /// + /// The default implementation returns a string like "...". + /// + /// + /// This method is called by the base implementation of the + /// method for arguments that are multi-value arguments. + /// + /// + protected virtual void WriteMultiValueSuffix() + => Write(Resources.DefaultArraySuffix); + + /// + /// Writes the help messages for any attributes + /// applied to the arguments class. + /// + /// + /// + /// The base implementation writes each message on its own line, followed by a blank line. + /// + /// + /// This method is called by the base implementation of the + /// method if the property is . + /// + /// + protected virtual void WriteClassValidators() + { + Writer.Indent = 0; + bool hasHelp = false; + foreach (var validator in Parser.Validators) + { + var help = validator.GetUsageHelp(Parser); + if (!string.IsNullOrEmpty(help)) + { + hasHelp = true; + WriteLine(help); + } + } + + if (hasHelp) + { + WriteLine(); // Blank line. + } + } - /// - /// Gets or sets a value that overrides the default application executable name used in the - /// usage syntax. - /// - /// - /// The application executable name, or to use the default value, - /// determined by calling . - /// - /// -#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - [AllowNull] -#endif - public virtual string ExecutableName + /// + /// Writes the list of argument descriptions. + /// + /// + /// + /// The default implementation gets the list of arguments using the + /// method, and calls the method for each one. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteArgumentDescriptions() + { + if (ArgumentDescriptionListFilter == DescriptionListFilterMode.None) { - get => _executableName ?? (_defaultExecutableName ??= CommandLineParser.GetExecutableName(IncludeExecutableExtension)); - set => _executableName = value; + return; } - /// - /// Gets or sets a value that indicates whether the usage syntax should include the file - /// name extension of the application's executable. - /// - /// - /// if the extension should be included; otherwise, . - /// The default value is . - /// - /// - /// - /// If the property is , the executable - /// name is determined by calling , - /// passing the value of this property as the argument. - /// - /// - /// This property is not used if the property is not - /// . - /// - /// - public bool IncludeExecutableExtension + if (ShouldIndent) { - get => _includeExecutableExtension; - set + // For long/short mode, increase the indentation by the size of the short argument. + Writer.Indent = ArgumentDescriptionIndent; + if (Parser.Mode == ParsingMode.LongShort) { - _includeExecutableExtension = value; - _defaultExecutableName = null; + Writer.Indent += Parser.ArgumentNamePrefixes[0].Length + NameSeparator.Length + 1; } } - /// - /// Gets or sets the color applied by the method. - /// - /// - /// The virtual terminal sequence for a color. The default value is - /// . - /// - /// - /// - /// The color will only be used if the property is - /// . - /// - /// - /// If the string contains anything other than virtual terminal sequences, those parts - /// will be included in the output, but only when the property is - /// . - /// - /// - /// The portion of the string that has color will end with the value of the - /// property. - /// - /// - /// With the base implementation, only the "Usage:" portion of the string has color; the - /// executable name does not. - /// - /// - public string UsagePrefixColor { get; set; } = TextFormat.ForegroundCyan; - - /// - /// Gets or sets the number of characters by which to indent all except the first line of the command line syntax of the usage help. - /// - /// - /// The number of characters by which to indent the usage syntax. The default value is the - /// value of the constant. - /// - /// - /// - /// The command line syntax is a single line that consists of the usage prefix written - /// by followed by the syntax of all - /// the arguments. This indentation is used when that line exceeds the maximum line - /// length. - /// - /// - /// This value is not used if the maximum line length of the to which the usage - /// is being written is less than 30. - /// - /// - public int SyntaxIndent { get; set; } = DefaultSyntaxIndent; - - /// - /// Gets or sets a value that indicates whether the usage syntax should use short names - /// for arguments that have one. - /// - /// - /// to use short names for arguments that have one; otherwise, - /// to use an empty string. The default value is - /// . - /// - /// - /// - /// This property is only used when the property is - /// . - /// - /// - public bool UseShortNamesForSyntax { get; set; } - - /// - /// Gets or sets a value that indicates whether to list only positional arguments in the - /// usage syntax. - /// - /// - /// to abbreviate the syntax; otherwise, . - /// The default value is . - /// - /// - /// - /// Abbreviated usage syntax only lists the positional arguments explicitly. After that, - /// if there are any more arguments, it will just print the value from the - /// method. The user will have to refer - /// to the description list to see the remaining possible - /// arguments. - /// - /// - /// Use this if your application has a very large number of arguments. - /// - /// - public bool UseAbbreviatedSyntax { get; set; } - - /// - /// Gets or sets the number of characters by which to indent all but the first line of each - /// argument's description, if the property is - /// . - /// - /// - /// The number of characters by which to indent the argument descriptions. The default - /// value is the value of the constant. - /// - /// - /// - /// This property is used by the method. - /// - /// - /// This value is not used if the maximum line length of the to which the usage - /// is being written is less than 30. - /// - /// - public int ArgumentDescriptionIndent { get; set; } = DefaultArgumentDescriptionIndent; - - /// - /// Gets or sets a value that indicates which arguments should be included in the list of - /// argument descriptions. - /// - /// - /// One of the values of the enumeration. The default - /// value is . - /// - public DescriptionListFilterMode ArgumentDescriptionListFilter { get; set; } - - /// - /// Gets or sets a value that indicates the order of the arguments in the list of argument - /// descriptions. - /// - /// - /// One of the values of the enumeration. The default - /// value is . - /// - public DescriptionListSortMode ArgumentDescriptionListOrder { get; set; } - - /// - /// Gets or sets the color applied by the method. - /// - /// - /// The virtual terminal sequence for a color. The default value is - /// . - /// - /// - /// - /// The color will only be used if the property is - /// . - /// - /// - /// If the string contains anything other than virtual terminal sequences, those parts - /// will be included in the output, but only when the property is - /// . - /// - /// - /// The portion of the string that has color will end with the value of the - /// property. - /// - /// - /// With the default format, only the argument name, value description and aliases - /// portion of the string has color; the actual argument description does not. - /// - /// - public string ArgumentDescriptionColor { get; set; } = TextFormat.ForegroundGreen; - - /// - /// Gets or sets a value indicating whether white space, rather than the value of the - /// property, is used to separate - /// arguments and their values in the command line syntax. - /// - /// - /// if the command line syntax uses a white space value separator; if it uses a colon. - /// The default value is . - /// - /// - /// - /// If this property is , an argument would be formatted in the command line syntax as "-name <Value>" (using - /// default formatting), with a white space character separating the argument name and value description. If this property is , - /// it would be formatted as "-name:<Value>", using a colon as the separator. - /// - /// - /// The command line syntax will only use a white space character as the value separator if both the property - /// and the property are true. - /// - /// - public bool UseWhiteSpaceValueSeparator { get; set; } = true; - - /// - /// Gets or sets a value indicating whether the alias or aliases of an argument should be included in the argument description.. - /// - /// - /// if the alias(es) should be included in the description; - /// otherwise, . The default value is . - /// - /// - /// - /// For arguments that do not have any aliases, this property has no effect. - /// - /// - public bool IncludeAliasInDescription { get; set; } = true; - - /// - /// Gets or sets a value indicating whether the default value of an argument should be included in the argument description. - /// - /// - /// if the default value should be included in the description; - /// otherwise, . The default value is . - /// - /// - /// - /// For arguments with a default value of , this property has no effect. - /// - /// - public bool IncludeDefaultValueInDescription { get; set; } = true; - - /// - /// Gets or sets a value indicating whether the - /// attributes of an argument should be included in the argument description. - /// - /// - /// if the validator descriptions should be included in; otherwise, - /// . The default value is . - /// - /// - /// - /// For arguments with no validators, or validators with no usage help, this property - /// has no effect. - /// - /// - public bool IncludeValidatorsInDescription { get; set; } = true; - - /// - /// Gets or sets a value indicating whether the - /// method will write a blank lines between arguments in the description list. - /// - /// - /// to write a blank line; otherwise, . The - /// default value is . - /// - public bool BlankLineAfterDescription { get; set; } = true; - - /// - /// Gets or sets the sequence used to reset color applied a usage help element. - /// - /// - /// The virtual terminal sequence used to reset color. The default value is - /// . - /// - /// - /// - /// This property will only be used if the property is - /// . - /// - /// - /// If the string contains anything other than virtual terminal sequences, those parts - /// will be included in the output, but only when the property is - /// . - /// - /// - public string ColorReset { get; set; } = TextFormat.Default; - - /// - /// Gets or sets the name of the subcommand. - /// - /// - /// The name of the subcommand, or if the current parser is not for - /// a subcommand. - /// - /// - /// - /// This property is set by the class before writing usage - /// help for a subcommand. - /// - /// - public string? CommandName { 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; - - /// - /// Gets or sets the color applied by the base implementation of the - /// method. - /// - /// - /// The virtual terminal sequence for a color. The default value is - /// . - /// - /// - /// - /// The color will only be used if the property is - /// . - /// - /// - /// If the string contains anything other than virtual terminal sequences, those parts - /// will be included in the output, but only when the property is - /// . - /// - /// - /// The portion of the string that has color will end with the . - /// - /// - /// With the default value, only the command name portion of the string has color; the - /// application name does not. - /// - /// - public string CommandDescriptionColor { get; set; } = TextFormat.ForegroundGreen; - - /// - /// Gets or sets the number of characters by which to indent the all but the first line of command descriptions. - /// - /// - /// The number of characters by which to indent the all but the first line of command descriptions. The default value is 8. - /// - /// - /// - /// This value is used by the base implementation of the - /// class, unless the property is . - /// - /// - public int CommandDescriptionIndent { get; set; } = DefaultCommandDescriptionIndent; - - /// - /// Gets or sets a value indicating whether the - /// method will write a blank lines between commands in the command list. - /// - /// - /// to write a blank line; otherwise, . The - /// default value is . - /// - public bool BlankLineAfterCommandDescription { get; set; } = true; + var arguments = GetArgumentsInDescriptionOrder(); + bool first = true; + foreach (var argument in arguments) + { + if (first) + { + WriteArgumentDescriptionListHeader(); + first = false; + } - /// - /// Gets or sets a value that indicates whether a message is shown at the bottom of the - /// command list that instructs the user how to get help for individual commands. - /// - /// - /// to show the instruction; otherwise, . - /// The default value is . - /// - /// - /// - /// If set to , the message is provided by the - /// method. The default implementation of that method assumes that all commands have a - /// help argument, the same , and the same argument prefixes. For - /// that reason, showing this message is not enabled by default. - /// - /// - public bool IncludeCommandHelpInstruction { get; set; } + WriteArgumentDescription(argument); + } + } - /// - /// Gets or sets a value that indicates whether to show the application description before - /// the command list in the usage help. - /// - /// - /// to show the description; otherwise, . The - /// default value is . - /// - /// - /// - /// The description to show is taken from the - /// of the first assembly passed to the class. If the - /// assembly has no description, nothing is written. - /// - /// - public bool IncludeApplicationDescriptionBeforeCommandList { get; set; } + /// + /// Writes a header before the list of argument descriptions. + /// + /// + /// + /// The base implementation does not write anything, as a header is not used in the + /// default format. + /// + /// + /// This method is called by the base implementation of the + /// method before the first argument. + /// + /// + protected virtual void WriteArgumentDescriptionListHeader() + { + // Intentionally blank. + } - /// - /// Gets or sets a value that indicates whether to show a command's aliases as part of the - /// command list usage help. - /// - /// - /// to show the command's aliases; otherwise, . - /// The default value is . - /// - public bool IncludeCommandAliasInCommandList { get; set; } = true; + /// + /// Writes the description of a single argument. + /// + /// The argument + /// + /// + /// The base implementation calls the method, + /// the method, and then adds an extra blank + /// line if the property is . + /// + /// + /// If color is enabled, the property is used for + /// the first line. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteArgumentDescription(CommandLineArgument argument) + { + WriteArgumentDescriptionHeader(argument); + WriteArgumentDescriptionBody(argument); - /// - /// Gets the to which the usage should be written. - /// - /// - /// The passed to the - /// constructor, or an instance created by the - /// or - /// function. - /// - /// - /// No was passed to the constructor, and a - /// operation is not in progress. - /// - protected LineWrappingTextWriter Writer - => _writer ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + if (BlankLineAfterDescription) + { + WriteLine(); + } + } - /// - /// Gets the that usage is being written for. - /// - /// - /// A operation is not in progress. - /// - protected CommandLineParser Parser - => _parser ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + /// + /// Writes the header of an argument's description, which is usually the name and value + /// description. + /// + /// The argument + /// + /// + /// The base implementation writes the name(s), value description, and alias(es), ending + /// with a new line. Which elements are included can be influenced using the properties of + /// this class. + /// + /// + /// If color is enabled, the property is used. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteArgumentDescriptionHeader(CommandLineArgument argument) + { + Writer.ResetIndent(); + var indent = ShouldIndent ? ArgumentDescriptionIndent : 0; + WriteSpacing(indent / 2); - /// - /// Gets the that usage is being written for. - /// - /// - /// A operation is not in progress. - /// - protected CommandManager CommandManager - => _commandManager ?? throw new InvalidOperationException(Resources.UsageWriterPropertyNotAvailable); + var shortPrefix = argument.Parser.ArgumentNamePrefixes[0]; + var prefix = argument.Parser.LongArgumentNamePrefix ?? shortPrefix; - /// - /// Indicates what operation is currently in progress. - /// - /// - /// One of the values of the enumeration. - /// - /// - /// - /// If this property is not , the - /// property will throw an exception. - /// - /// - /// If this property is not , the - /// property will throw an exception. - /// - /// - /// If this property is , the - /// property may throw an exception. - /// - /// - protected Operation OperationInProgress + WriteColor(ArgumentDescriptionColor); + if (argument.Parser.Mode == ParsingMode.LongShort) { - get + if (argument.HasShortName) { - if (_parser != null) - { - return Operation.ParserUsage; - } - else if (_commandManager != null) + WriteArgumentNameForDescription(argument.ShortName.ToString(), shortPrefix); + if (argument.HasLongName) { - return Operation.CommandListUsage; + Write(NameSeparator); } + } + else + { + WriteSpacing(shortPrefix.Length + NameSeparator.Length + 1); + } - return Operation.None; + if (argument.HasLongName) + { + WriteArgumentNameForDescription(argument.ArgumentName, prefix); } } - - /// - /// Gets a value that indicates whether indentation should be enabled in the output. - /// - /// - /// if the property's maximum line length is - /// unlimited or greater than 30; otherwise, . - /// - /// - /// No was passed to the constructor, and a - /// operation is not in progress. - /// - protected virtual bool ShouldIndent => Writer.MaximumLineLength is 0 or >= MinimumLineWidthForIndent; - - /// - /// Gets the separator used for argument names, command names, and aliases. - /// - /// - /// The string ", ". - /// - protected virtual string NameSeparator => ", "; - - /// - /// Creates usage help for the specified parser. - /// - /// The . - /// The parts of usage to write. - /// - /// is . - /// - /// - /// - /// If no writer was passed to the - /// constructor, this method will create a for the - /// standard output stream. If color usage wasn't explicitly enabled, it will be enabled - /// if the output supports it according to . - /// - /// - /// This method calls the method to create the usage help - /// text. - /// - /// - public void WriteParserUsage(CommandLineParser parser, UsageHelpRequest request = UsageHelpRequest.Full) + else { - _parser = parser ?? throw new ArgumentNullException(nameof(parser)); - WriteUsageInternal(request); + WriteArgumentNameForDescription(argument.ArgumentName, prefix); } - /// - /// Creates usage help for the specified command manager. - /// - /// The - /// - /// is . - /// - /// - /// - /// The usage help will contain a list of all available commands. - /// - /// - /// If no writer was passed to the - /// constructor, this method will create a for the - /// standard output stream. If color usage wasn't explicitly enabled, it will be enabled - /// if the output supports it according to . - /// - /// - /// This method calls the method to create the - /// usage help text. - /// - /// - public void WriteCommandListUsage(CommandManager manager) + Write(' '); + if (argument.IsSwitch) { - _commandManager = manager ?? throw new ArgumentNullException(nameof(manager)); - WriteUsageInternal(); + WriteSwitchValueDescription(argument.ValueDescription); } - - /// - /// Returns a string with usage help for the specified parser. - /// - /// A string containing the usage help. - /// The . - /// The parts of usage to write. - /// - /// The length at which to white-space wrap lines in the output, or 0 to disable wrapping. - /// - /// - /// is . - /// - /// - /// - /// This method ignores the writer passed to the - /// constructor, and will use the - /// method instead, and returns the resulting string. If color support was not explicitly - /// enabled, it will be disabled. - /// - /// - /// This method calls the method to create the usage help - /// text. - /// - /// - public string GetUsage(CommandLineParser parser, UsageHelpRequest request = UsageHelpRequest.Full, int maximumLineLength = 0) + else { - _parser = parser ?? throw new ArgumentNullException(nameof(parser)); - return GetUsageInternal(maximumLineLength, request); + WriteValueDescriptionForDescription(argument.ValueDescription); } - /// - /// Returns a string with usage help for the specified command manager. - /// - /// A string containing the usage help. - /// The - /// - /// The length at which to white-space wrap lines in the output, or 0 to disable wrapping. - /// - /// - /// is . - /// - /// - /// - /// The usage help will contain a list of all available commands. - /// - /// - /// This method ignores the writer passed to the - /// constructor, and will use the - /// method instead, and returns the resulting string. If color support was not explicitly - /// enabled, it will be disabled. - /// - /// - /// This method calls the method to create the - /// usage help text. - /// - /// - public string GetCommandListUsage(CommandManager manager, int maximumLineLength = 0) + if (IncludeAliasInDescription) { - _commandManager = manager ?? throw new ArgumentNullException(nameof(manager)); - return GetUsageInternal(maximumLineLength); + WriteAliases(argument.Aliases, argument.ShortAliases, prefix, shortPrefix); } - #region CommandLineParser usage + ResetColor(); + WriteLine(); + } - /// - /// Creates the usage help for a instance. - /// - /// The parts of usage to write. - /// - /// - /// This is the primary method used to generate usage help for the - /// class. It calls into the various other methods of this class, so overriding this - /// method should not typically be necessary unless you wish to deviate from the order - /// in which usage elements are written. - /// - /// - /// The base implementation writes the application description, followed by the usage - /// syntax, followed by the class validator help messages, followed by a list of argument - /// descriptions. Which elements are included exactly can be influenced by the - /// parameter and the properties of this class. - /// - /// - protected virtual void WriteParserUsageCore(UsageHelpRequest request) + /// + /// Writes the body of an argument description, which is usually the description itself + /// with any supplemental information. + /// + /// The argument. + /// + /// + /// The base implementation writes the description text, argument validator messages, and + /// the default value, followed by two new lines. Which elements are included can be + /// influenced using the properties of this class. + /// + /// + protected virtual void WriteArgumentDescriptionBody(CommandLineArgument argument) + { + bool hasDescription = !string.IsNullOrEmpty(argument.Description); + if (hasDescription) { - if (request == UsageHelpRequest.None) - { - WriteMoreInfoMessage(); - return; - } - - if (request == UsageHelpRequest.Full && IncludeApplicationDescription && !string.IsNullOrEmpty(Parser.Description)) - { - WriteApplicationDescription(Parser.Description); - } - - WriteParserUsageSyntax(); - if (request == UsageHelpRequest.Full) - { - if (IncludeValidatorsInDescription) - { - WriteClassValidators(); - } - - WriteArgumentDescriptions(); - Writer.Indent = 0; - } - else - { - Writer.Indent = 0; - WriteMoreInfoMessage(); - } + WriteArgumentDescription(argument.Description); } - /// - /// Writes the application description, or command description in case of a subcommand. - /// - /// The description. - /// - /// - /// This method is called by the base implementation of the - /// method if the command has a description and the - /// property is . - /// - /// - /// This method is called by the base implementation of the - /// method if the assembly has a description and the - /// property is . - /// - /// - protected virtual void WriteApplicationDescription(string description) + if (IncludeValidatorsInDescription) { - SetIndent(ApplicationDescriptionIndent); - WriteLine(description); - WriteLine(); + WriteArgumentValidators(argument); } - /// - /// Writes the usage syntax for the application or subcommand. - /// - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteParserUsageSyntax() + if (IncludeDefaultValueInDescription && argument.IncludeDefaultInUsageHelp && argument.DefaultValue != null) { - Writer.ResetIndent(); - SetIndent(SyntaxIndent); + WriteDefaultValue(argument.DefaultValue); + } - WriteUsageSyntaxPrefix(); - foreach (CommandLineArgument argument in Parser.Arguments) - { - if (argument.IsHidden) - { - continue; - } + WriteLine(); + } - Write(" "); - if (UseAbbreviatedSyntax && argument.Position == null) - { - WriteAbbreviatedRemainingArguments(); - break; - } + /// + /// Writes the name or alias of an argument for use in the argument description list. + /// + /// The argument name or alias. + /// + /// The argument name prefix; if using , this may vary + /// depending on whether the name or alias is a short or long name or alias. + /// + /// + /// + /// The default implementation returns the prefix followed by the name. + /// + /// + /// This method is called by the base implementation of the + /// method and the method. + /// + /// + protected virtual void WriteArgumentNameForDescription(string argumentName, string prefix) + { + Write(prefix); + Write(argumentName); + } - if (argument.IsRequired) - { - WriteArgumentSyntax(argument); - } - else - { - WriteOptionalArgumentSyntax(argument); - } - } + /// + /// Writes the value description of an argument for use in the argument description list. + /// + /// The value description. + /// + /// + /// The base implementation returns the value description surrounded by angle brackets. + /// For example, "<String>". + /// + /// + /// This method is called by the base implementation of the + /// method and by the method.. + /// + /// + protected virtual void WriteValueDescriptionForDescription(string valueDescription) + => Write($"<{valueDescription}>"); + + /// + /// Writes the value description of a switch argument for use in the argument description + /// list. + /// + /// The value description. + /// + /// + /// The default implementation surrounds the value written by the + /// method with square brackets, to indicate that it is optional. + /// + /// + /// This method is called by the base implementation of the + /// method for switch arguments. + /// + /// + protected virtual void WriteSwitchValueDescription(string valueDescription) + { + Write(OptionalStart); + WriteValueDescriptionForDescription(valueDescription); + Write(OptionalEnd); + } - WriteLine(); // End syntax line - WriteLine(); // Blank line + /// + /// Writes the aliases of an argument for use in the argument description list. + /// + /// + /// The aliases of an argument, or the long aliases for + /// mode, or if the argument has no (long) aliases. + /// + /// + /// The short aliases of an argument, or if the argument has no short + /// aliases. + /// + /// + /// The argument name prefix to use for the . + /// + /// + /// The argument name prefix to use for the . + /// + /// + /// + /// The base implementation writes a list of the short aliases, followed by the long + /// aliases, surrounded by parentheses, and preceded by a single space. For example, + /// " (-Alias1, -Alias2)" or " (-a, -b, --alias1, --alias2)". + /// + /// + /// If there are no aliases at all, it writes nothing. + /// + /// + /// This method is called by the base implementation of the + /// method if the property is . + /// + /// + protected virtual void WriteAliases(IEnumerable? aliases, IEnumerable? shortAliases, string prefix, string shortPrefix) + { + if (shortAliases == null && aliases == null) + { + return; + } + + var count = WriteAliasHelper(shortPrefix, shortAliases, 0); + count = WriteAliasHelper(prefix, aliases, count); + + if (count > 0) + { + Write(")"); } + } + + /// + /// Writes a single alias for use in the argument description list. + /// + /// The alias. + /// + /// The argument name prefix; if using , this may vary + /// depending on whether the alias is a short or long alias. + /// + /// + /// + /// The base implementation calls the method. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteAlias(string alias, string prefix) + => WriteArgumentNameForDescription(alias, prefix); + + /// + /// Writes the actual argument description text. + /// + /// The description. + /// + /// + /// The base implementation just writes the description text. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteArgumentDescription(string description) + { + Write(description); + } - /// - /// Write the prefix for the usage syntax, including the executable name and, for - /// subcommands, the command name. - /// - /// - /// - /// The base implementation returns a string like "Usage: executable" or "Usage: - /// executable command", using the color specified. If color is enabled, part of the - /// string will be colored using the property. - /// - /// - /// An implementation of this method should typically include the value of the - /// property, and the value of the - /// property if it's not . - /// - /// - /// This method is called by the base implementation of the - /// method and the method. - /// - /// - protected virtual void WriteUsageSyntaxPrefix() + /// + /// Writes the help message of any attributes + /// applied to the argument. + /// + /// The argument. + /// + /// + /// The base implementation writes each message separated by a space, and preceded by a + /// space. + /// + /// + /// This method is called by the base implementation of the + /// method if the property is + /// . + /// + /// + protected virtual void WriteArgumentValidators(CommandLineArgument argument) + { + foreach (var validator in argument.Validators) { - WriteColor(UsagePrefixColor); - Write(Resources.DefaultUsagePrefix); - ResetColor(); - Write(' '); - Write(ExecutableName); - if (CommandName != null) + var help = validator.GetUsageHelp(argument); + if (!string.IsNullOrEmpty(help)) { Write(' '); - Write(CommandName); + Write(help); } } + } - /// - /// Writes the syntax for a single optional argument. - /// - /// The argument. - /// - /// - /// The base implementation surrounds the result of the - /// method in square brackets. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteOptionalArgumentSyntax(CommandLineArgument argument) - { - Write(OptionalStart); - WriteArgumentSyntax(argument); - Write(OptionalEnd); - } + /// + /// Writes the default value of an argument. + /// + /// The default value. + /// + /// + /// The base implementation writes a string like " Default value: value.", including the + /// leading space. + /// + /// + /// This method is called by the base implementation of the + /// method if the property is + /// and the property + /// is not . + /// + /// + protected virtual void WriteDefaultValue(object defaultValue) + => Write(Resources.DefaultDefaultValueFormat, defaultValue); - /// - /// Writes the syntax for a single argument. - /// - /// The argument. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteArgumentSyntax(CommandLineArgument argument) + /// + /// Writes a message telling to user how to get more detailed help. + /// + /// + /// + /// The default implementation writes a message like "Run 'executable -Help' for more + /// information." or "Run 'executable command -Help' for more information." + /// + /// + /// If the property returns , + /// nothing is written. + /// + /// + /// This method is called by the base implementation of the + /// method if the requested help is not . + /// + /// + protected virtual void WriteMoreInfoMessage() + { + var arg = Parser.HelpArgument; + if (arg != null) { - string argumentName; - if (argument.HasShortName && UseShortNamesForSyntax) - { - argumentName = argument.ShortName.ToString(); - } - else + var name = ExecutableName; + if (CommandName != null) { - argumentName = argument.ArgumentName; + name += " " + CommandName; } - var prefix = argument.Parser.Mode != ParsingMode.LongShort || (argument.HasShortName && (UseShortNamesForSyntax || !argument.HasLongName)) - ? argument.Parser.ArgumentNamePrefixes[0] - : argument.Parser.LongArgumentNamePrefix!; - - char? separator = argument.Parser.AllowWhiteSpaceValueSeparator && UseWhiteSpaceValueSeparator - ? null - : argument.Parser.NameValueSeparator; + WriteLine(Resources.MoreInfoOnErrorFormat, name, arg.ArgumentNameWithPrefix); + } + } - if (argument.Position == null) - { - WriteArgumentName(argumentName, prefix); - } - else - { - WritePositionalArgumentName(argumentName, prefix, separator); - } + /// + /// Gets the parser's arguments filtered according to the + /// property and sorted according to the property. + /// + /// A list of filtered and sorted arguments. + /// + /// + /// Arguments that are hidden are excluded from the list. + /// + /// + protected virtual IEnumerable GetArgumentsInDescriptionOrder() + { + var arguments = Parser.Arguments.Where(argument => !argument.IsHidden && ArgumentDescriptionListFilter switch + { + DescriptionListFilterMode.Information => argument.HasInformation(this), + DescriptionListFilterMode.Description => !string.IsNullOrEmpty(argument.Description), + DescriptionListFilterMode.All => true, + _ => false, + }); - if (!argument.IsSwitch) - { - // Otherwise, the separator was included in the argument name. - if (argument.Position == null || separator == null) - { - Write(separator ?? ' '); - } + var comparer = Parser.ArgumentNameComparison.GetComparer(); - WriteValueDescription(argument.ValueDescription); - } + return ArgumentDescriptionListOrder switch + { + DescriptionListSortMode.Alphabetical => arguments.OrderBy(arg => arg.ArgumentName, comparer), + DescriptionListSortMode.AlphabeticalDescending => arguments.OrderByDescending(arg => arg.ArgumentName, comparer), + DescriptionListSortMode.AlphabeticalShortName => + arguments.OrderBy(arg => arg.HasShortName ? arg.ShortName.ToString() : arg.ArgumentName, comparer), + DescriptionListSortMode.AlphabeticalShortNameDescending => + arguments.OrderByDescending(arg => arg.HasShortName ? arg.ShortName.ToString() : arg.ArgumentName, comparer), + _ => arguments, + }; + } - if (argument.IsMultiValue) - { - WriteMultiValueSuffix(); - } - } + #endregion - /// - /// Writes the name of an argument. - /// - /// The name of the argument. - /// - /// The argument name prefix; if using , this may vary - /// depending on whether the name is a short or long name. - /// - /// - /// - /// The default implementation returns the prefix followed by the name, e.g. "-Name". - /// - /// - /// This method is called by the base implementation of the - /// method and the method. - /// - /// - protected virtual void WriteArgumentName(string argumentName, string prefix) - { - Write(prefix); - Write(argumentName); - } + #region Subcommand usage - /// - /// Writes the name of a positional argument. - /// - /// The name of the argument. - /// - /// The argument name prefix; if using , this may vary - /// depending on whether the name is a short or long name. - /// - /// - /// The argument name/value separator, or if the - /// property and the property - /// are both . - /// - /// - /// - /// The default implementation surrounds the value written by the - /// method, as well as the if not , - /// with square brackets. For example, "[-Name]" or "[-Name:]", to indicate the name - /// itself is optional. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WritePositionalArgumentName(string argumentName, string prefix, char? separator) + /// + /// Creates the usage help for a instance. + /// + /// + /// + /// This is the primary method used to generate usage help for the + /// class. It calls into the various other methods of this class, so overriding this + /// method should not typically be necessary unless you wish to deviate from the order + /// in which usage elements are written. + /// + /// + /// 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. + /// + /// + protected virtual void WriteCommandListUsageCore() + { + if (IncludeApplicationDescriptionBeforeCommandList) { - Write(OptionalStart); - WriteArgumentName(argumentName, prefix); - if (separator is char separatorValue) + var description = CommandManager.GetApplicationDescription(); + if (description != null) { - Write(separatorValue); + WriteApplicationDescription(description); } - - Write(OptionalEnd); } - /// - /// Writes the value description of an argument. - /// - /// The value description. - /// - /// - /// The base implementation returns the value description surrounded by angle brackets. - /// For example, "<String>". - /// - /// - /// This method is called by the base implementation of the - /// method for arguments that are not switch arguments. - /// - /// - protected virtual void WriteValueDescription(string valueDescription) - => Write($"<{valueDescription}>"); - - /// - /// Writes the string used to indicate there are more arguments if the usage syntax was - /// abbreviated. - /// - /// - /// - /// The default implementation returns a string like "[arguments]". - /// - /// - /// This method is called by the base implementation of the - /// method if the property is . - /// - /// - protected virtual void WriteAbbreviatedRemainingArguments() - => Write(Resources.DefaultAbbreviatedRemainingArguments); + SetIndent(SyntaxIndent); + WriteCommandListUsageSyntax(); + Writer.ResetIndent(); + Writer.Indent = 0; + WriteAvailableCommandsHeader(); - /// - /// Writes a suffix that indicates an argument is a multi-value argument. - /// - /// - /// - /// The default implementation returns a string like "...". - /// - /// - /// This method is called by the base implementation of the - /// method for arguments that are multi-value arguments. - /// - /// - protected virtual void WriteMultiValueSuffix() - => Write(Resources.DefaultArraySuffix); + WriteCommandDescriptions(); - /// - /// Writes the help messages for any attributes - /// applied to the arguments class. - /// - /// - /// - /// The base implementation writes each message on its own line, followed by a blank line. - /// - /// - /// This method is called by the base implementation of the - /// method if the property is . - /// - /// - protected virtual void WriteClassValidators() + 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; - bool hasHelp = false; - foreach (var validator in Parser.Validators) + var name = ExecutableName; + if (CommandName != null) { - var help = validator.GetUsageHelp(Parser); - if (!string.IsNullOrEmpty(help)) - { - hasHelp = true; - WriteLine(help); - } + name += " " + CommandName; } - if (hasHelp) - { - WriteLine(); // Blank line. - } + WriteCommandHelpInstruction(name, prefix, argumentName); } + } - /// - /// Writes the list of argument descriptions. - /// - /// - /// - /// The default implementation gets the list of arguments using the - /// method, and calls the method for each one. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteArgumentDescriptions() - { - if (ArgumentDescriptionListFilter == DescriptionListFilterMode.None) - { - return; - } + /// + /// Writes the usage syntax for an application using subcommands. + /// + /// + /// + /// The base implementation calls and . + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteCommandListUsageSyntax() + { + WriteUsageSyntaxPrefix(); + WriteUsageSyntaxSuffix(); + WriteLine(); + } - if (ShouldIndent) - { - // For long/short mode, increase the indentation by the size of the short argument. - Writer.Indent = ArgumentDescriptionIndent; - if (Parser.Mode == ParsingMode.LongShort) - { - Writer.Indent += Parser.ArgumentNamePrefixes[0].Length + NameSeparator.Length + 1; - } - } + /// + /// Writes a header before the list of available commands. + /// + /// + /// + /// The base implementation writes a string like "The following commands are available:" + /// followed by a blank line. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteAvailableCommandsHeader() + { + WriteLine(Resources.DefaultAvailableCommandsHeader); + WriteLine(); + } - var arguments = GetFilteredAndSortedArguments(); - bool first = true; - foreach (var argument in arguments) + /// + /// Writes a list of available commands. + /// + /// + /// + /// The base implementation calls for all commands, + /// except hidden commands. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteCommandDescriptions() + { + SetIndent(CommandDescriptionIndent); + foreach (var command in CommandManager.GetCommands()) + { + if (command.IsHidden) { - if (first) - { - WriteArgumentDescriptionListHeader(); - first = false; - } - - WriteArgumentDescription(argument); + continue; } - } - /// - /// Writes a header before the list of argument descriptions. - /// - /// - /// - /// The base implementation does not write anything, as a header is not used in the - /// default format. - /// - /// - /// This method is called by the base implementation of the - /// method before the first argument. - /// - /// - protected virtual void WriteArgumentDescriptionListHeader() - { - // Intentionally blank. + WriteCommandDescription(command); } + } - /// - /// Writes the description of a single argument. - /// - /// The argument - /// - /// - /// The base implementation calls the method, - /// the method, and then adds an extra blank - /// line if the property is . - /// - /// - /// If color is enabled, the property is used for - /// the first line. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteArgumentDescription(CommandLineArgument argument) - { - WriteArgumentDescriptionHeader(argument); - WriteArgumentDescriptionBody(argument); + /// + /// Writes the description of a command. + /// + /// The command. + /// + /// + /// The base implementation calls the method, + /// the method, and then adds an extra blank + /// line if the property is . + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteCommandDescription(CommandInfo command) + { + WriteCommandDescriptionHeader(command); + WriteCommandDescriptionBody(command); - if (BlankLineAfterDescription) - { - WriteLine(); - } + if (BlankLineAfterCommandDescription) + { + WriteLine(); } + } - /// - /// Writes the header of an argument's description, which is usually the name and value - /// description. - /// - /// The argument - /// - /// - /// The base implementation writes the name(s), value description, and alias(es), ending - /// with a new line. Which elements are included can be influenced using the properties of - /// this class. - /// - /// - /// If color is enabled, the property is used. - /// - /// - /// This method is called by the base implementation of . - /// - /// - protected virtual void WriteArgumentDescriptionHeader(CommandLineArgument argument) + /// + /// Writes the header of a command's description, which is typically the name and alias(es) + /// of the command. + /// + /// The command. + /// + /// + /// The base implementation writes the command's name and alias(es), using the color from + /// the property if color is enabled, followed by a + /// newline. + /// + /// + protected virtual void WriteCommandDescriptionHeader(CommandInfo command) + { + Writer.ResetIndent(); + var indent = ShouldIndent ? CommandDescriptionIndent : 0; + WriteSpacing(indent / 2); + WriteColor(CommandDescriptionColor); + WriteCommandName(command.Name); + if (IncludeCommandAliasInCommandList) { - Writer.ResetIndent(); - var indent = ShouldIndent ? ArgumentDescriptionIndent : 0; - WriteSpacing(indent / 2); - - var shortPrefix = argument.Parser.ArgumentNamePrefixes[0]; - var prefix = argument.Parser.LongArgumentNamePrefix ?? shortPrefix; - - WriteColor(ArgumentDescriptionColor); - if (argument.Parser.Mode == ParsingMode.LongShort) - { - if (argument.HasShortName) - { - WriteArgumentNameForDescription(argument.ShortName.ToString(), shortPrefix); - if (argument.HasLongName) - { - Write(NameSeparator); - } - } - else - { - WriteSpacing(shortPrefix.Length + NameSeparator.Length + 1); - } - - if (argument.HasLongName) - { - WriteArgumentNameForDescription(argument.ArgumentName, prefix); - } - } - else - { - WriteArgumentNameForDescription(argument.ArgumentName, prefix); - } - - Write(' '); - if (argument.IsSwitch) - { - WriteSwitchValueDescription(argument.ValueDescription); - } - else - { - WriteValueDescriptionForDescription(argument.ValueDescription); - } + WriteCommandAliases(command.Aliases); + } - if (IncludeAliasInDescription) - { - WriteAliases(argument.Aliases, argument.ShortAliases, prefix, shortPrefix); - } + ResetColor(); + WriteLine(); + } - ResetColor(); + /// + /// Writes the body of a command's description, which is typically the description of the + /// command. + /// + /// The command. + /// + /// + /// The base implementation writes the command's description, followed by a newline. + /// + /// + protected virtual void WriteCommandDescriptionBody(CommandInfo command) + { + if (command.Description != null) + { + WriteCommandDescription(command.Description); WriteLine(); } + } - /// - /// Writes the body of an argument description, which is usually the description itself - /// with any supplemental information. - /// - /// The argument. - /// - /// - /// The base implementation writes the description text, argument validator messages, and - /// the default value, followed by two new lines. Which elements are included can be - /// influenced using the properties of this class. - /// - /// - protected virtual void WriteArgumentDescriptionBody(CommandLineArgument argument) - { - bool hasDescription = !string.IsNullOrEmpty(argument.Description); - if (hasDescription) - { - WriteArgumentDescription(argument.Description); - } - - if (IncludeValidatorsInDescription) - { - WriteArgumentValidators(argument); - } - - if (IncludeDefaultValueInDescription && argument.DefaultValue != null) - { - WriteDefaultValue(argument.DefaultValue); - } + /// + /// Writes the name of a command. + /// + /// The command name. + /// + /// + /// The base implementation just writes the name. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteCommandName(string commandName) + => Write(commandName); - WriteLine(); + /// + /// Writes the aliases of a command. + /// + /// The aliases. + /// + /// + /// The default implementation writes a comma-separated list of aliases, preceded by a + /// comma. + /// + /// + /// This method is called by the base implementation of the + /// method if the property is . + /// + /// + protected virtual void WriteCommandAliases(IEnumerable aliases) + { + foreach (var alias in aliases) + { + Write(NameSeparator); + Write(alias); } + } + + /// + /// Writes the description text of a command. + /// + /// The description. + /// + /// + /// The base implementation just writes the description text. + /// + /// + /// This method is called by the base implementation of the + /// method. + /// + /// + protected virtual void WriteCommandDescription(string description) + => Write(description); - /// - /// Writes the name or alias of an argument for use in the argument description list. - /// - /// The argument name or alias. - /// - /// The argument name prefix; if using , this may vary - /// depending on whether the name or alias is a short or long name or alias. - /// - /// - /// - /// The default implementation returns the prefix followed by the name. - /// - /// - /// This method is called by the base implementation of the - /// method and the method. - /// - /// - protected virtual void WriteArgumentNameForDescription(string argumentName, string prefix) - { - Write(prefix); - Write(argumentName); - } + /// + /// Writes an instruction on how to get help on a command. + /// + /// The application and command name. + /// The argument name prefix for a help argument. + /// The automatic help argument name. + /// + /// + /// The base implementation writes a string like "Run 'executable command -Help' for more + /// information on a command." + /// + /// + /// 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); + } - /// - /// Writes the value description of an argument for use in the argument description list. - /// - /// The value description. - /// - /// - /// The base implementation returns the value description surrounded by angle brackets. - /// For example, "<String>". - /// - /// - /// This method is called by the base implementation of the - /// method and by the method.. - /// - /// - protected virtual void WriteValueDescriptionForDescription(string valueDescription) - => Write($"<{valueDescription}>"); + #endregion - /// - /// Writes the value description of a switch argument for use in the argument description - /// list. - /// - /// The value description. - /// - /// - /// The default implementation surrounds the value written by the - /// method with angle brackets, to indicate that it is optional. - /// - /// - /// This method is called by the base implementation of the - /// method for switch arguments. - /// - /// - protected virtual void WriteSwitchValueDescription(string valueDescription) + /// + /// Writes the specified amount of spaces to the . + /// + /// The number of spaces. + protected virtual void WriteSpacing(int count) + { + for (int i = 0; i < count; ++i) { - Write(OptionalStart); - WriteValueDescriptionForDescription(valueDescription); - Write(OptionalEnd); + Write(' '); } + } - /// - /// Writes the aliases of an argument for use in the argument description list. - /// - /// - /// The aliases of an argument, or the long aliases for - /// mode, or if the argument has no (long) aliases. - /// - /// - /// The short aliases of an argument, or if the argument has no short - /// aliases. - /// - /// - /// The argument name prefix to use for the . - /// - /// - /// The argument name prefix to use for the . - /// - /// - /// - /// The base implementation writes a list of the short aliases, followed by the long - /// aliases, surrounded by parentheses, and preceded by a single space. For example, - /// " (-Alias1, -Alias2)" or " (-a, -b, --alias1, --alias2)". - /// - /// - /// If there are no aliases at all, it writes nothing. - /// - /// - /// This method is called by the base implementation of the - /// method if the property is . - /// - /// - protected virtual void WriteAliases(IEnumerable? aliases, IEnumerable? shortAliases, string prefix, string shortPrefix) - { - if (shortAliases == null && aliases == null) - { - return; - } - - var count = WriteAliasHelper(shortPrefix, shortAliases, 0); - count = WriteAliasHelper(prefix, aliases, count); + /// + /// Writes a string to the . + /// + /// The string to write. + /// + /// + /// This method, along with the method, is called for every write by + /// the base implementation. Override this method if you need to apply a transformation, like + /// HTML encoding, to all written text. + /// + /// + protected virtual void Write(string? value) => Writer.Write(value); - if (count > 0) - { - Write(")"); - } - } + /// + /// Writes a character to the . + /// + /// The character to write. + /// + /// + /// This method, along with the method, is called for every write + /// by the base implementation. Override this method if you need to apply a transformation, + /// like HTML encoding, to all written text. + /// + /// + protected virtual void Write(char value) => Writer.Write(value); - /// - /// Writes a single alias for use in the argument description list. - /// - /// The alias. - /// - /// The argument name prefix; if using , this may vary - /// depending on whether the alias is a short or long alias. - /// - /// - /// - /// The base implementation calls the method. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteAlias(string alias, string prefix) - => WriteArgumentNameForDescription(alias, prefix); + /// + /// Writes a new line to the . + /// + /// + /// + /// This method is called for every explicit new line added by the base implementation. + /// Override this method if you need to apply a transformation to all newlines. + /// + /// + /// This method does not get called for newlines embedded in strings like argument + /// descriptions. Those will be part of strings passed to the + /// method. + /// + /// + protected virtual void WriteLine() => Writer.WriteLine(); - /// - /// Writes the actual argument description text. - /// - /// The description. - /// - /// - /// The base implementation just writes the description text. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteArgumentDescription(string description) - { - Write(description); - } - /// - /// Writes the help message of any attributes - /// applied to the argument. - /// - /// The argument. - /// - /// - /// The base implementation writes each message separated by a space, and preceded by a - /// space. - /// - /// - /// This method is called by the base implementation of the - /// method if the property is - /// . - /// - /// - protected virtual void WriteArgumentValidators(CommandLineArgument argument) + /// + /// Writes a string with virtual terminal sequences only if color is enabled. + /// + /// The color formatting. + /// + /// + /// Nothing is written if the property is . + /// + /// + protected void WriteColor(TextFormat color) + { + if (UseColor) { - foreach (var validator in argument.Validators) - { - var help = validator.GetUsageHelp(argument); - if (!string.IsNullOrEmpty(help)) - { - Write(' '); - Write(help); - } - } + Write(color.Value); } + } - /// - /// Writes the default value of an argument. - /// - /// The default value. - /// - /// - /// The base implementation writes a string like " Default value: value.", including the - /// leading space. - /// - /// - /// This method is called by the base implementation of the - /// method if the property is - /// and the property - /// is not . - /// - /// - protected virtual void WriteDefaultValue(object defaultValue) - => Write(Resources.DefaultDefaultValueFormat, defaultValue); + /// + /// Returns the output color to the value before modifications, if color is enabled. + /// + /// + /// + /// Writes the value of the property if color is enabled. + /// + /// + /// Nothing is written if the property is . + /// + /// + protected void ResetColor() => WriteColor(ColorReset); - /// - /// Writes a message telling to user how to get more detailed help. - /// - /// - /// - /// The default implementation writes a message like "Run 'executable -Help' for more - /// information." or "Run 'executable command -Help' for more information." - /// - /// - /// If the property returns , - /// nothing is written. - /// - /// - /// This method is called by the base implementation of the - /// method if the requested help is not . - /// - /// - protected virtual void WriteMoreInfoMessage() + /// + /// Sets the indentation of the , only if the + /// property returns . + /// + /// The number of characters to use for indentation. + protected void SetIndent(int indent) + { + if (ShouldIndent) { - var arg = Parser.HelpArgument; - if (arg != null) - { - var name = ExecutableName; - if (CommandName != null) - { - ExecutableName += " " + CommandName; - } - - WriteLine(Resources.MoreInfoOnErrorFormat, name, arg.ArgumentNameWithPrefix); - } + Writer.Indent = indent; } + } - /// - /// Gets the parser's arguments filtered according to the - /// property and sorted according to the property. - /// - /// A list of filtered and sorted arguments. - /// - /// - /// Arguments that are hidden are excluded from the list. - /// - /// - protected IEnumerable GetFilteredAndSortedArguments() + internal string GetArgumentUsage(CommandLineArgument argument) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + _writer = writer; + _parser = argument.Parser; + if (argument.IsRequired) { - var arguments = Parser.Arguments.Where(argument => !argument.IsHidden && ArgumentDescriptionListFilter switch - { - DescriptionListFilterMode.Information => argument.HasInformation(this), - DescriptionListFilterMode.Description => !string.IsNullOrEmpty(argument.Description), - DescriptionListFilterMode.All => true, - _ => false, - }); - - var comparer = Parser.ArgumentNameComparer; - return ArgumentDescriptionListOrder switch - { - DescriptionListSortMode.Alphabetical => arguments.OrderBy(arg => arg.ArgumentName, comparer), - DescriptionListSortMode.AlphabeticalDescending => arguments.OrderByDescending(arg => arg.ArgumentName, comparer), - DescriptionListSortMode.AlphabeticalShortName => - arguments.OrderBy(arg => arg.HasShortName ? arg.ShortName.ToString() : arg.ArgumentName, comparer), - DescriptionListSortMode.AlphabeticalShortNameDescending => - arguments.OrderByDescending(arg => arg.HasShortName ? arg.ShortName.ToString() : arg.ArgumentName, comparer), - _ => arguments, - }; + WriteArgumentSyntax(argument); } - - #endregion - - #region Subcommand usage - - /// - /// Creates the usage help for a instance. - /// - /// - /// - /// This is the primary method used to generate usage help for the - /// class. It calls into the various other methods of this class, so overriding this - /// method should not typically be necessary unless you wish to deviate from the order - /// in which usage elements are written. - /// - /// - /// 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. - /// - /// - protected virtual void WriteCommandListUsageCore() + else { - if (IncludeApplicationDescriptionBeforeCommandList) - { - var description = CommandManager.GetApplicationDescription(); - if (description != null) - { - WriteApplicationDescription(description); - } - } + WriteOptionalArgumentSyntax(argument); + } - SetIndent(SyntaxIndent); - WriteCommandListUsageSyntax(); - Writer.ResetIndent(); - Writer.Indent = 0; - WriteAvailableCommandsHeader(); + writer.Flush(); + return writer.BaseWriter.ToString()!; + } - WriteCommandDescriptions(); + private void WriteLine(string? value) + { + Write(value); + WriteLine(); + } - if (IncludeCommandHelpInstruction) - { - var prefix = CommandManager.Options.Mode == ParsingMode.LongShort - ? (CommandManager.Options.LongArgumentNamePrefix ?? CommandLineParser.DefaultLongArgumentNamePrefix) - : (CommandManager.Options.ArgumentNamePrefixes?.FirstOrDefault() ?? CommandLineParser.GetDefaultArgumentNamePrefixes()[0]); + private void Write(string format, object? arg0) => Write(string.Format(Writer.FormatProvider, format, arg0)); - var transform = CommandManager.Options.ArgumentNameTransform ?? NameTransform.None; - var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); + private void WriteLine(string format, object? arg0, object? arg1) + => WriteLine(string.Format(Writer.FormatProvider, format, arg0, arg1)); - Writer.Indent = 0; - WriteCommandHelpInstruction(prefix, argumentName); - } - } + private void WriteLine(string format, object? arg0, object? arg1, object? arg2) + => WriteLine(string.Format(Writer.FormatProvider, format, arg0, arg1, arg2)); - /// - /// Writes the usage syntax for an application using subcommands. - /// - /// - /// - /// The base implementation calls , and adds to it - /// a string like " <command> [arguments]". - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteCommandListUsageSyntax() + private VirtualTerminalSupport? EnableColor() + { + if (_useColor == null && _writer == null) { - WriteUsageSyntaxPrefix(); - WriteLine(Resources.DefaultCommandUsageSuffix); - WriteLine(); + var support = VirtualTerminal.EnableColor(StandardStream.Output); + _useColor = support.IsSupported; + return support; } - /// - /// Writes a header before the list of available commands. - /// - /// - /// - /// The base implementation writes a string like "The following commands are available:" - /// followed by a blank line. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteAvailableCommandsHeader() - { - WriteLine(Resources.DefaultAvailableCommandsHeader); - WriteLine(); - } + return null; + } - /// - /// Writes a list of available commands. - /// - /// - /// - /// The base implementation calls for all commands, - /// except hidden commands. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteCommandDescriptions() + private int WriteAliasHelper(string prefix, IEnumerable? aliases, int count) + { + if (aliases == null) { - SetIndent(CommandDescriptionIndent); - foreach (var command in CommandManager.GetCommands()) - { - if (command.IsHidden) - { - continue; - } - - WriteCommandDescription(command); - } + return count; } - /// - /// Writes the description of a command. - /// - /// The command. - /// - /// - /// The base implementation calls the method, - /// the method, and then adds an extra blank - /// line if the property is . - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteCommandDescription(CommandInfo command) + foreach (var alias in aliases) { - WriteCommandDescriptionHeader(command); - WriteCommandDescriptionBody(command); - - if (BlankLineAfterCommandDescription) + if (count == 0) { - WriteLine(); + Write(" ("); } - } - - /// - /// Writes the header of a command's description, which is typically the name and alias(es) - /// of the command. - /// - /// The command. - /// - /// - /// The base implementation writes the command's name and alias(es), using the color from - /// the property if color is enabled, followed by a - /// newline. - /// - /// - protected virtual void WriteCommandDescriptionHeader(CommandInfo command) - { - Writer.ResetIndent(); - var indent = ShouldIndent ? CommandDescriptionIndent : 0; - WriteSpacing(indent / 2); - WriteColor(CommandDescriptionColor); - WriteCommandName(command.Name); - if (IncludeCommandAliasInCommandList) + else { - WriteCommandAliases(command.Aliases); + Write(NameSeparator); } - ResetColor(); - WriteLine(); + WriteAlias(alias!.ToString()!, prefix); + ++count; } - /// - /// Writes the body of a command's description, which is typically the description of the - /// command. - /// - /// The command. - /// - /// - /// The base implementation writes the command's description, followed by a newline. - /// - /// - protected virtual void WriteCommandDescriptionBody(CommandInfo command) + return count; + } + + private void WriteUsageInternal(UsageHelpRequest request = UsageHelpRequest.Full) + { + bool restoreColor = _useColor == null; + bool restoreWriter = _writer == null; + try { - if (command.Description != null) - { - WriteCommandDescription(command.Description); - WriteLine(); - } + using var support = EnableColor(); + using var writer = DisposableWrapper.Create(_writer, LineWrappingTextWriter.ForConsoleOut); + _writer = writer.Inner; + Writer.ResetIndent(); + Writer.Indent = 0; + RunOperation(request); } - - /// - /// Writes the name of a command. - /// - /// The command name. - /// - /// - /// The base implementation just writes the name. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteCommandName(string commandName) - => Write(commandName); - - /// - /// Writes the aliases of a command. - /// - /// The aliases. - /// - /// - /// The default implementation writes a comma-separated list of aliases, preceded by a - /// comma. - /// - /// - /// This method is called by the base implementation of the - /// method if the property is . - /// - /// - protected virtual void WriteCommandAliases(IEnumerable aliases) + finally { - foreach (var alias in aliases) + if (restoreColor) { - Write(NameSeparator); - Write(alias); + _useColor = null; } - } - - /// - /// Writes the description text of a command. - /// - /// The description. - /// - /// - /// The base implementation just writes the description text. - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteCommandDescription(string description) - => Write(description); - - /// - /// Writes an instruction on how to get help on a command. - /// - /// The argument name prefix for a help argument. - /// The automatic help argument name. - /// - /// - /// The base implementation writes a string like "Run 'executable command -Help' for more - /// information on a command." - /// - /// - /// This method is called by the base implementation of the - /// method if the property is . - /// If that property is , it is assumed that every command has an - /// argument matching the automatic help argument's name. - /// - /// - protected virtual void WriteCommandHelpInstruction(string argumentNamePrefix, string argumentName) - => WriteLine(Resources.CommandHelpInstructionFormat, ExecutableName, argumentNamePrefix, argumentName); - - #endregion - /// - /// Writes the specified amount of spaces to the . - /// - /// The number of spaces. - protected virtual void WriteSpacing(int count) - { - for (int i = 0; i < count; ++i) + if (restoreWriter) { - Write(' '); + _writer = null; } } + } - /// - /// Writes a string to the . - /// - /// The string to write. - /// - /// - /// This method, along with , is called for every write by the - /// base implementation. Override this method if you need to apply a transformation, - /// like HTML encoding, to all written text. - /// - /// - protected virtual void Write(string? value) => Writer.Write(value); - - /// - /// Writes a character to the . - /// - /// The character to write. - /// - /// - /// This method, along with , is called for every write by the - /// base implementation. Override this method if you need to apply a transformation, - /// like HTML encoding, to all written text. - /// - /// - protected virtual void Write(char value) => Writer.Write(value); - - /// - /// Writes a new line to the . - /// - /// - /// - /// This method is called for every explicit new line added by the base implementation. - /// Override this method if you need to apply a transformation to all newlines. - /// - /// - /// This method does not get called for newlines embedded in strings like argument - /// descriptions. Those will be part of strings passed to the - /// method. - /// - /// - protected virtual void WriteLine() => Writer.WriteLine(); - - - /// - /// Writes a string with virtual terminal sequences only if color is enabled. - /// - /// The string containing the color formatting. - /// - /// - /// The should contain one or more virtual terminal sequences - /// from the class, or another virtual terminal sequence. It - /// should not contain any other characters. - /// - /// - /// Nothing is written if the property is . - /// - /// - protected void WriteColor(string color) + private string GetUsageInternal(int maximumLineLength = 0, UsageHelpRequest request = UsageHelpRequest.Full) + { + var originalWriter = _writer; + try { - if (UseColor) - { - Write(color); - } + using var writer = LineWrappingTextWriter.ForStringWriter(maximumLineLength); + _writer = writer; + RunOperation(request); + writer.Flush(); + return writer.BaseWriter.ToString()!; } - - /// - /// Returns the color to the previous value, if color is enabled. - /// - /// - /// - /// Writes the value of the property if color is enabled. - /// - /// - /// Nothing is written if the property is . - /// - /// - protected void ResetColor() => WriteColor(ColorReset); - - /// - /// Sets the indentation of the , only if the - /// property returns . - /// - /// The number of characters to use for indentation. - protected void SetIndent(int indent) + finally { - if (ShouldIndent) - { - Writer.Indent = indent; - } + _writer = originalWriter; } + } - internal string GetArgumentUsage(CommandLineArgument argument) + private void RunOperation(UsageHelpRequest request) + { + try { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - _writer = writer; - _parser = argument.Parser; - if (argument.IsRequired) + if (_parser == null) { - WriteArgumentSyntax(argument); + WriteCommandListUsageCore(); } else { - WriteOptionalArgumentSyntax(argument); + WriteParserUsageCore(request); } - - writer.Flush(); - return writer.BaseWriter.ToString()!; } - - private void WriteLine(string? value) + finally { - Write(value); - WriteLine(); + _parser = null; + _commandManager = null; } + } - 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() + private bool CheckShowCommandHelpInstruction() + { + if (IncludeCommandHelpInstruction is bool include) { - if (_useColor == null && _writer == null) - { - var support = VirtualTerminal.EnableColor(StandardStream.Output); - _useColor = support.IsSupported; - return support; - } - - return null; + return include; } - private int WriteAliasHelper(string prefix, IEnumerable? aliases, int count) + // If not automatically set, check requirements from all commands. + if (CommandManager.Options.AutoHelpArgument == false) { - if (aliases == null) - { - return count; - } - - foreach (var alias in aliases) - { - if (count == 0) - { - Write(" ("); - } - else - { - Write(NameSeparator); - } - - WriteAlias(alias!.ToString()!, prefix); - ++count; - } - - return count; + return false; } - private void WriteUsageInternal(UsageHelpRequest request = UsageHelpRequest.Full) + // Options specified in ParseOptions override the ParseOptionsAttribute so those won't + // need to be checked. + var globalMode = CommandManager.Options.Mode != null; + var globalNameTransform = CommandManager.Options.ArgumentNameTransform != null; + var globalPrefixes = CommandManager.Options.ArgumentNamePrefixes != null; + var globalLongPrefix = CommandManager.Options.LongArgumentNamePrefix != null; + ParsingMode actualMode = default; + ParsingMode? requiredMode = null; + NameTransform? requiredNameTransform = null; + bool first = true; + foreach (var command in CommandManager.GetCommands()) { - bool restoreColor = _useColor == null; - bool restoreWriter = _writer == null; - try + if (command.UseCustomArgumentParsing) { - 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; - } + return false; } - } - 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 + var options = command.CommandType.GetCustomAttribute() ?? new(); + if (first) { - _writer = originalWriter; + requiredMode ??= options.Mode; + requiredNameTransform ??= options.ArgumentNameTransform; + actualMode = CommandManager.Options.Mode ?? options.Mode; + first = false; } - } - private void RunOperation(UsageHelpRequest request) - { - try - { - if (_parser == null) - { - WriteCommandListUsageCore(); - } - else - { - WriteParserUsageCore(request); - } - } - finally + if ((!globalMode && requiredMode != options.Mode) || + (!globalNameTransform && requiredNameTransform != options.ArgumentNameTransform) || + (!globalPrefixes && options.ArgumentNamePrefixes != null) || + (actualMode == ParsingMode.LongShort && !globalLongPrefix && options.LongArgumentNamePrefix != null)) { - _parser = null; - _commandManager = null; + return false; } } + + return true; } } diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index abebfdea..6493f63a 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -1,140 +1,214 @@ using Ookii.CommandLine.Commands; using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Base class for argument validators. +/// +/// +/// +/// Argument validators are executed before or after an argument's value is set, and allow +/// you to check whether an argument's value meets certain conditions. +/// +/// +/// If validation fails, the validator will throw a +/// with the category specified in the property. The +/// method, the +/// method, +/// the generated , +/// and the class will automatically display the error message and +/// usage help if validation failed. +/// +/// +/// Several built-in validators are provided, and you can derive from this class to create +/// custom validators. +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Parameter)] +public abstract class ArgumentValidationAttribute : Attribute { /// - /// Base class for argument validators. + /// Gets a value that indicates when validation will run. + /// + /// + /// One of the values of the enumeration. If not overridden + /// in a derived class, the value is . + /// + public virtual ValidationMode Mode => ValidationMode.AfterConversion; + + /// + /// Gets the error category used for the when + /// validation fails. + /// + /// + /// One of the values of the enumeration. If not overridden + /// in a derived class, the value is . + /// + public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; + + /// + /// Validates the argument value, and throws an exception if validation failed. + /// + /// The argument being validated. + /// + /// The argument value. If not , this must be a string or an instance of + /// . + /// + /// + /// The parameter is not a valid value. The + /// property will be the value of the property. + /// + public void Validate(CommandLineArgument argument, object? value) + { + if (argument == null) + { + throw new ArgumentNullException(nameof(argument)); + } + + if (!IsValid(argument, value)) + { + throw new CommandLineArgumentException(GetErrorMessage(argument, value), argument.ArgumentName, ErrorCategory); + } + } + + /// + /// Validates the argument value, and throws an exception if validation failed. /// + /// The argument being validated. + /// + /// The argument value. If not , this must be an instance of + /// . + /// + /// + /// if validation was performed and successful; + /// if this validator doesn't support validating spans and the + /// method should be used instead. + /// /// /// - /// Argument validators are executed before or after an argument's value is set, and allow - /// you to check whether an argument's value meets certain conditions. - /// - /// - /// If validation fails, it will throw a with - /// the category specified in the property. The - /// method, the - /// method and the - /// class will automatically display the error message and usage - /// help if validation failed. - /// - /// - /// Several built-in validators are provided, and you can derive from this class to create - /// custom validators. + /// The class will only call this method if the + /// property is . /// /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Parameter)] - public abstract class ArgumentValidationAttribute : Attribute + /// + /// The parameter is not a valid value. The + /// property will be the value of the property. + /// + public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) { - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// One of the values of the enumeration. If not overridden - /// in a derived class, the value is . - /// - public virtual ValidationMode Mode => ValidationMode.AfterConversion; - - /// - /// Gets the error category used for the when - /// validation fails. - /// - /// - /// One of the values of the enumeration. If not overridden - /// in a derived class, the value is . - /// - public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; - - /// - /// Validates the argument value, and throws an exception if validation failed. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// The parameter is not a valid value. The - /// property will be the value of the property. - /// - public void Validate(CommandLineArgument argument, object? value) + if (argument == null) { - if (argument == null) - { - throw new ArgumentNullException(nameof(argument)); - } + throw new ArgumentNullException(nameof(argument)); + } - if (!IsValid(argument, value)) - { - throw new CommandLineArgumentException(GetErrorMessage(argument, value), argument.ArgumentName, ErrorCategory); - } + var result = IsSpanValid(argument, value); + if (result == false) + { + throw new CommandLineArgumentException(GetErrorMessage(argument, value.ToString()), argument.ArgumentName, ErrorCategory); } - /// - /// When overridden in a derived class, determines if the argument is valid. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - /// - /// - /// For regular arguments, the parameter will be identical to - /// the property. For multi-value or dictionary - /// arguments, the parameter will equal the last value added - /// to the collection or dictionary. - /// - /// - /// If the property is , - /// will always be . Use the - /// property instead. - /// - /// - /// If you need to check the type of the argument, use the - /// property unless you want to get the collection type for a multi-value or dictionary - /// argument. - /// - /// - public abstract bool IsValid(CommandLineArgument argument, object? value); + return result != null; + } - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// The error message. - /// - /// - /// Override this method in a derived class to provide a custom error message. Otherwise, - /// it will return a generic error message. - /// - /// - public virtual string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidationFailed(argument.ArgumentName); - /// - /// Gets the usage help message for this validator. - /// - /// The argument is the validator is for. - /// - /// The usage help message, or if there is none. The - /// base implementation always returns . - /// - /// - /// - /// This function is only called if the - /// property is . - /// - /// - public virtual string? GetUsageHelp(CommandLineArgument argument) => null; - } + /// + /// When overridden in a derived class, determines if the argument is valid. + /// + /// The argument being validated. + /// + /// The argument value. If not , this must be a string or an + /// instance of . + /// + /// + /// if the value is valid; otherwise, . + /// + /// + /// + /// If the property is , + /// the parameter will be the raw string value provided by the + /// user on the command line. + /// + /// + /// If the property is , + /// for regular arguments, the parameter will be identical to + /// the property. For multi-value or dictionary + /// arguments, the parameter will be equal to the last value added + /// to the collection or dictionary. + /// + /// + /// If the property is , + /// will always be . Use the + /// property instead. + /// + /// + /// If you need to check the type of the argument, use the + /// property unless you want to get the collection type for a multi-value or dictionary + /// argument. + /// + /// + public abstract bool IsValid(CommandLineArgument argument, object? value); + + /// + /// When overridden in a derived class, determines if the argument is valid. + /// + /// The argument being validated. + /// + /// The raw string argument value provided by the user on the command line. + /// + /// + /// if this validator doesn't support validating spans, and the + /// regular method should be called instead; + /// if the value is valid; otherwise, . + /// + /// + /// + /// The class will only call this method if the + /// property is . + /// + /// + /// If you need to check the type of the argument, use the + /// property unless you want to get the collection type for a multi-value or dictionary + /// argument. + /// + /// + /// The base class implementation returns . + /// + /// + public virtual bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) => null; + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// + /// The argument value. If not , this must be an instance of + /// . + /// + /// The error message. + /// + /// + /// Override this method in a derived class to provide a custom error message. Otherwise, + /// it will return a generic error message. + /// + /// + public virtual string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidationFailed(argument.ArgumentName); + + /// + /// Gets the usage help message for this validator. + /// + /// The argument that the validator is for. + /// + /// The usage help message, or if there is none. The + /// base implementation always returns . + /// + /// + /// + /// This function is only called if the + /// property is . + /// + /// + public virtual string? GetUsageHelp(CommandLineArgument argument) => null; } diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs index eb593a47..84509c9e 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs @@ -1,62 +1,62 @@ -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Base class for argument validators that have usage help. +/// +/// +/// +/// It's not required for argument validators that have help to derive from this class; +/// it's sufficient to derive from the class +/// directly and override the method. +/// This class just adds some common functionality to make it easier. +/// +/// +/// +public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAttribute { /// - /// Base class for argument validators that have usage help. + /// Gets or sets a value that indicates whether this validator's help should be included + /// in the argument's description. /// + /// + /// to include it in the description; otherwise, . + /// The default value is . + /// /// /// - /// It's not required for argument validators that have help to derive from this class; - /// it's sufficient to derive from the class - /// directly and override the method. - /// This class just adds some common functionality to make it easier. + /// This has no effect if the + /// property is . + /// + /// + /// The help text is the value returned by . /// /// - public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAttribute - { - /// - /// Gets or sets a value that indicates whether this validator's help should be included - /// in the argument's description. - /// - /// - /// to include it in the description; otherwise, . - /// The default value is . - /// - /// - /// - /// This has no effect if the - /// property is . - /// - /// - /// The help text is the value returned by . - /// - /// - public bool IncludeInUsageHelp { get; set; } = true; + public bool IncludeInUsageHelp { get; set; } = true; - /// - /// Gets the usage help message for this validator. - /// - /// The argument is the validator is for. - /// - /// The usage help message, or if the - /// property is . - /// - /// - /// - /// This function is only called if the - /// property is . - /// - /// + /// + /// Gets the usage help message for this validator. + /// + /// The argument that the validator is for. + /// + /// The usage help message, or if the + /// property is . + /// + /// + /// + /// This function is only called if the + /// property is . + /// + /// - public override string? GetUsageHelp(CommandLineArgument argument) - => IncludeInUsageHelp ? GetUsageHelpCore(argument) : null; + public override string? GetUsageHelp(CommandLineArgument argument) + => IncludeInUsageHelp ? GetUsageHelpCore(argument) : null; - /// - /// Gets the usage help message for this validator. - /// - /// The argument is the validator is for. - /// - /// The usage help message. - /// - protected abstract string GetUsageHelpCore(CommandLineArgument argument); - } + /// + /// Gets the usage help message for this validator. + /// + /// The argument that the validator is for. + /// + /// The usage help message. + /// + protected abstract string GetUsageHelpCore(CommandLineArgument argument); } diff --git a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs index f860ab1f..17412a08 100644 --- a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs @@ -1,104 +1,104 @@ using Ookii.CommandLine.Commands; using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Base class for argument class validators. +/// +/// +/// +/// Class validators are executed when all arguments have been parsed, and allow you to check +/// whether the whole set of arguments meets a condition. Use this instead of +/// if the type of validation being performed doesn't belong to a specific argument, or must +/// be performed even if the argument(s) don't have values. +/// +/// +/// If validation fails, the validator will throw a +/// with the category specified in the property. The +/// method, the +/// method, +/// the generated , +/// and the class will automatically display the error message and +/// usage help if validation failed. +/// +/// +/// A built-in validator is provided, and you can derive from this class to create custom +/// validators. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public abstract class ClassValidationAttribute : Attribute { /// - /// Base class for argument class validators. + /// Gets the error category used for the when + /// validation fails. /// - /// - /// - /// Class validators are executed when all arguments have been parsed, and allow you to check - /// whether the whole set of arguments meets a condition. Use this instead of - /// if the type of validation being performed doesn't belong to a specific argument, or must - /// be performed even if the argument(s) don't have values. - /// - /// - /// If validation fails, it will throw a with - /// the category specified in the property. The - /// method, the - /// method and the - /// class will automatically display the error message and usage - /// help if validation failed. - /// - /// - /// A built-in validator is provided, and you can derive from this class to create custom - /// validators. - /// - /// - /// - [AttributeUsage(AttributeTargets.Class)] - public abstract class ClassValidationAttribute : Attribute - { - /// - /// Gets the error category used for the when - /// validation fails. - /// - /// - /// One of the values of the enumeration. If not overridden - /// in a derived class, the value is . - /// - public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; + /// + /// One of the values of the enumeration. If not overridden + /// in a derived class, the value is . + /// + public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; - /// - /// Validates the argument value, and throws an exception if validation failed. - /// - /// The argument parser being validated. - /// - /// The combination of arguments in the is not valid. The - /// property will be the value of the - /// property. - /// - public void Validate(CommandLineParser parser) + /// + /// Validates the argument value, and throws an exception if validation failed. + /// + /// The argument parser being validated. + /// + /// The combination of arguments in the is not valid. The + /// property will be the value of the + /// property. + /// + public void Validate(CommandLineParser parser) + { + if (parser == null) { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + throw new ArgumentNullException(nameof(parser)); + } - if (!IsValid(parser)) - { - throw new CommandLineArgumentException(GetErrorMessage(parser), null, ErrorCategory); - } + if (!IsValid(parser)) + { + throw new CommandLineArgumentException(GetErrorMessage(parser), null, ErrorCategory); } + } - /// - /// Gets the error message to display if validation failed. - /// - /// The argument parser that was validated. - /// The error message. - /// - /// - /// Override this method in a derived class to provide a custom error message. Otherwise, - /// it will return a generic error message. - /// - /// - public virtual string GetErrorMessage(CommandLineParser parser) - => parser.StringProvider.ClassValidationFailed(); + /// + /// Gets the error message to display if validation failed. + /// + /// The command line parser that was validated. + /// The error message. + /// + /// + /// Override this method in a derived class to provide a custom error message. Otherwise, + /// it will return a generic error message. + /// + /// + public virtual string GetErrorMessage(CommandLineParser parser) + => parser.StringProvider.ClassValidationFailed(); - /// - /// When overridden in a derived class, determines if the arguments are valid. - /// - /// The argument parser being validated. - /// - /// if the arguments are valid; otherwise, . - /// - public abstract bool IsValid(CommandLineParser parser); + /// + /// When overridden in a derived class, determines if the arguments are valid. + /// + /// The command line parser being validated. + /// + /// if the arguments are valid; otherwise, . + /// + public abstract bool IsValid(CommandLineParser parser); - /// - /// Gets the usage help message for this validator. - /// - /// The parser is the validator is for. - /// - /// The usage help message, or if there is none. The - /// base implementation always returns . - /// - /// - /// - /// This function is only called if the - /// property is . - /// - /// - public virtual string? GetUsageHelp(CommandLineParser parser) => null; - } + /// + /// Gets the usage help message for this validator. + /// + /// The command line parser that the validator is for. + /// + /// The usage help message, or if there is none. The + /// base implementation always returns . + /// + /// + /// + /// This function is only called if the + /// property is . + /// + /// + public virtual string? GetUsageHelp(CommandLineParser parser) => null; } diff --git a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs index b204bf7f..bda73858 100644 --- a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs @@ -4,136 +4,136 @@ using System.Globalization; using System.Linq; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Base class for the and class. +/// +/// +public abstract class DependencyValidationAttribute : ArgumentValidationWithHelpAttribute { + private readonly string? _argument; + private readonly string[]? _arguments; + private readonly bool _requires; + /// - /// Base class for the and class. + /// Initializes a new instance of the class. /// - public abstract class DependencyValidationAttribute : ArgumentValidationWithHelpAttribute + /// + /// if this is a requires dependency, or + /// for a prohibits dependency. + /// + /// The name of the argument that this argument depends on. + /// + /// is . + /// + protected DependencyValidationAttribute(bool requires, string argument) { - private readonly string? _argument; - private readonly string[]? _arguments; - private readonly bool _requires; - - /// - /// Initializes a new instance of the class. - /// - /// - /// if this is a requires dependency, or - /// for a prohibits dependency. - /// - /// The name of the argument that this argument depends on. - /// - /// is . - /// - public DependencyValidationAttribute(bool requires, string argument) - { - _argument = argument ?? throw new ArgumentNullException(nameof(argument)); - _requires = requires; - } + _argument = argument ?? throw new ArgumentNullException(nameof(argument)); + _requires = requires; + } - /// - /// Initializes a new instance of the class with multiple - /// dependencies. - /// - /// - /// if this is a requires dependency, or - /// for a prohibits dependency. - /// - /// The names of the arguments that this argument depends on. - /// - /// is . - /// - public DependencyValidationAttribute(bool requires, params string[] arguments) - { - _arguments = arguments ?? throw new ArgumentNullException(nameof(arguments)); - _requires = requires; - } + /// + /// Initializes a new instance of the class with multiple + /// dependencies. + /// + /// + /// if this is a requires dependency, or + /// for a prohibits dependency. + /// + /// The names of the arguments that this argument depends on. + /// + /// is . + /// + protected DependencyValidationAttribute(bool requires, params string[] arguments) + { + _arguments = arguments ?? throw new ArgumentNullException(nameof(arguments)); + _requires = requires; + } - /// - /// Gets the names of the arguments that the validator checks against. - /// - /// - /// An array of argument names. - /// - public string[] Arguments => _arguments ?? new[] { _argument! }; + /// + /// Gets the names of the arguments that the validator checks against. + /// + /// + /// An array of argument names. + /// + public string[] Arguments => _arguments ?? new[] { _argument! }; - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// . - /// - public override ValidationMode Mode => ValidationMode.AfterParsing; + /// + /// Gets a value that indicates when validation will run. + /// + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.AfterParsing; - /// - /// Gets the error category used for the when - /// validation fails. - /// - /// - /// . - /// - public override CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.DependencyFailed; + /// + /// Gets the error category used for the when + /// validation fails. + /// + /// + /// . + /// + public override CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.DependencyFailed; - /// - /// Determines if the dependencies are met. - /// - /// The argument being validated. - /// Not used - /// - /// if the value is valid; otherwise, . - /// - /// - /// One of the argument names in the property refers to an - /// argument that doesn't exist. - /// - public sealed override bool IsValid(CommandLineArgument argument, object? value) + /// + /// Determines if the dependencies are met. + /// + /// The argument being validated. + /// Not used + /// + /// if the value is valid; otherwise, . + /// + /// + /// One of the argument names in the property refers to an + /// argument that doesn't exist. + /// + public sealed override bool IsValid(CommandLineArgument argument, object? value) + { + var args = GetArguments(argument.Parser); + if (_requires) { - var args = GetArguments(argument.Parser); - if (_requires) - { - return args.All(a => a.HasValue); - } - else - { - return args.All(a => !a.HasValue); - } + return args.All(a => a.HasValue); } - - /// - /// Resolves the argument names in the property to their actual - /// property. - /// - /// The instance. - /// A list of the arguments. - /// - /// is . - /// - /// - /// One of the argument names in the property refers to an - /// argument that doesn't exist. - /// - public IEnumerable GetArguments(CommandLineParser parser) + else { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } - - if (_argument != null) - { - var arg = parser.GetArgument(_argument) ?? throw GetUnknownDependencyException(_argument); - return Enumerable.Repeat(arg, 1); - } + return args.All(a => !a.HasValue); + } + } - Debug.Assert(_arguments != null); - return _arguments - .Select(name => parser.GetArgument(name) ?? throw GetUnknownDependencyException(name)); + /// + /// Resolves the argument names in the property to their actual + /// instances. + /// + /// The instance. + /// A list of the arguments. + /// + /// is . + /// + /// + /// One of the argument names in the property refers to an + /// argument that doesn't exist. + /// + public IEnumerable GetArguments(CommandLineParser parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); } - private InvalidOperationException GetUnknownDependencyException(string name) + if (_argument != null) { - return new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.UnknownDependencyFormat, GetType().Name, name)); + var arg = parser.GetArgument(_argument) ?? throw GetUnknownDependencyException(_argument); + return Enumerable.Repeat(arg, 1); } + + Debug.Assert(_arguments != null); + return _arguments + .Select(name => parser.GetArgument(name) ?? throw GetUnknownDependencyException(name)); + } + + private InvalidOperationException GetUnknownDependencyException(string name) + { + return new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.UnknownDependencyFormat, GetType().Name, name)); } } diff --git a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs index b854d47e..80bae5bd 100644 --- a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs @@ -1,67 +1,81 @@ using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that an argument is not used together with other arguments. +/// +/// +/// +/// This attribute can be used to indicate that an argument can only be used when one or more +/// other arguments are not used. If one or more of the prohibited arguments has a value, +/// validation will fail. +/// +/// +/// This validator will not be checked until all arguments have been parsed. +/// +/// +/// If validation fails, a is thrown with the +/// error category set to . +/// +/// +/// The names of arguments that are dependencies are not validated when the attribute is created. +/// If one of the specified arguments does not exist, an exception will be thrown during +/// validation. +/// +/// +/// +public class ProhibitsAttribute : DependencyValidationAttribute { /// - /// Validates that an argument cannot be used together with other arguments. + /// Initializes a new instance of the class. + /// + /// The name of the argument that this argument prohibits. + /// + /// is . + /// + public ProhibitsAttribute(string argument) + : base(false, argument) + { + } + + /// + /// Initializes a new instance of the class with multiple + /// prohibited arguments. /// + /// The names of the arguments that this argument prohibits. + /// + /// is . + /// + public ProhibitsAttribute(params string[] arguments) + : base(false, arguments) + { + } + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. /// /// - /// This attribute can be used to indicate that an argument can only be used in combination - /// with one or more other attributes. If one or more of the dependencies does not have - /// a value, validation will fail. - /// - /// - /// This validator will not be checked until all arguments have been parsed. - /// - /// - /// If validation fails, a is thrown with the - /// error category set to . + /// Use a custom class that overrides the + /// method + /// to customize this message. /// + /// + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateProhibitsFailed(argument.MemberName, GetArguments(argument.Parser)); + + /// + /// /// - /// Names of arguments that are dependencies are not validated when the attribute is created. - /// If one of the specified arguments does not exist, validation will always fail. + /// Use a custom class that overrides the + /// method + /// to customize this message. /// /// - /// - public class ProhibitsAttribute : DependencyValidationAttribute - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the argument that this argument prohibits. - /// - /// is . - /// - public ProhibitsAttribute(string argument) - : base(false, argument) - { - } - - /// - /// Initializes a new instance of the class with multiple - /// dependencies. - /// - /// The names of the arguments that this argument prohibits. - /// - /// is . - /// - public ProhibitsAttribute(params string[] arguments) - : base(false, arguments) - { - } - - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidateProhibitsFailed(argument.MemberName, GetArguments(argument.Parser)); - - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ProhibitsUsageHelp(GetArguments(argument.Parser)); - } + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ProhibitsUsageHelp(GetArguments(argument.Parser)); } diff --git a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs index 90dfe2cb..b2a92d3f 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs @@ -3,200 +3,213 @@ using System.Globalization; using System.Linq; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates whether at least one of the specified arguments is supplied. +/// +/// +/// +/// This is a class validator, which should be applied to the class that defines arguments, +/// not to a specific argument. +/// +/// +/// Use this attribute if you have multiple arguments, only one of which needs to be supplied +/// at a time. +/// +/// +/// This attribute is useful when combined with the attribute. +/// If you have two mutually exclusive attribute, you cannot mark either of them as required. +/// For example, given arguments A and B, if B prohibits A but A is required, then B can +/// never be used. +/// +/// +/// Instead, you can use the attribute to indicate that +/// the user must supply either A or B, and the attribute +/// to indicate that they cannot supply both at once. +/// +/// +/// [RequiresAny(nameof(Address), nameof(Path))] +/// class Arguments +/// { +/// [CommandLineArgument] +/// public Uri Address { get; set; } +/// +/// [CommandLineArgument] +/// [Prohibits(nameof(Address))] +/// public string Path { get; set; } +/// } +/// +/// +/// You can only use nameof if the name of the argument matches the name of the +/// property. Be careful if you have explicit names or are using a . +/// +/// +/// The names of the arguments are not validated when the attribute is created. If one of the +/// specified arguments does not exist, an exception is thrown during validation. +/// +/// +/// +public class RequiresAnyAttribute : ClassValidationAttribute { + private readonly string[] _arguments; + /// - /// Validates whether at least one of the specified arguments is supplied. + /// Initializes a new instance of the class. /// + /// The name of the first argument. + /// The name of the second argument. + /// + /// or is . + /// /// - /// - /// This is a class validator, which should be applied to the class that defines arguments, - /// not to a specific argument. - /// - /// - /// Use this attribute if you have multiple arguments, only one of which needs to be supplied - /// at a time. - /// - /// - /// This attribute is useful when combined with the attribute. - /// If you have two mutually exclusive attribute, you cannot mark either of them as required. - /// For example, given arguments A and B, if B prohibits A but A is required, then B can - /// never be used. - /// - /// - /// Instead, you can use the attribute to indicate that - /// the user must supply either A or B, and the attribute - /// to indicate that they cannot supply both at once. - /// - /// - /// [RequiresAny(nameof(Address), nameof(Path))] - /// class Arguments - /// { - /// [CommandLineArgument] - /// public Uri Address { get; set; } - /// - /// [CommandLineArgument] - /// [Prohibits(nameof(Address))] - /// public string Path { get; set; } - /// } - /// - /// - /// You can only use nameof if the name of the argument matches the name of the - /// property. Be careful if you have explicit names or are using . - /// - /// - /// The names of the arguments are not validated when the attribute is created. If one of the - /// specified arguments does not exist, it is assumed to have no value. - /// + /// This constructor exists because + /// is not CLS-compliant. /// - /// - public class RequiresAnyAttribute : ClassValidationAttribute + public RequiresAnyAttribute(string argument1, string argument2) { - private readonly string[] _arguments; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the first argument. - /// The name of the second argument. - /// - /// or is . - /// - /// - /// This constructor exists because - /// is not CLS-compliant. - /// - public RequiresAnyAttribute(string argument1, string argument2) + // This constructor exists to avoid a warning about non-CLS compliant types. + if (argument1 == null) { - // This constructor exists to avoid a warning about non-CLS compliant types. - if (argument1 == null) - { - throw new ArgumentNullException(nameof(argument1)); - } - - if (argument2 == null) - { - throw new ArgumentNullException(nameof(argument2)); - } - - _arguments = new[] { argument1, argument2 }; + throw new ArgumentNullException(nameof(argument1)); } - /// - /// Initializes a new instance of the class. - /// - /// The names of the arguments. - /// - /// or one of its items is . - /// - /// - /// contains fewer than two items. - /// - public RequiresAnyAttribute(params string[] arguments) + if (argument2 == null) { - if (_arguments == null || _arguments.Any(a => a == null)) - { - throw new ArgumentNullException(nameof(arguments)); - } + throw new ArgumentNullException(nameof(argument2)); + } - if (_arguments.Length <= 1) - { - throw new ArgumentException(Properties.Resources.RequiresAnySingleArgument, nameof(arguments)); - } + _arguments = new[] { argument1, argument2 }; + } - _arguments = arguments; + /// + /// Initializes a new instance of the class. + /// + /// The names of the arguments. + /// + /// or one of its items is . + /// + /// + /// contains fewer than two items. + /// + public RequiresAnyAttribute(params string[] arguments) + { + if (_arguments == null || _arguments.Any(a => a == null)) + { + throw new ArgumentNullException(nameof(arguments)); } - /// - /// Gets the names of the arguments, one of which must be supplied on the command line. - /// - /// - /// The names of the arguments. - /// - public string[] Arguments => _arguments; - - /// - /// Gets the error category used for the when - /// validation fails. - /// - /// - /// . - /// - public override CommandLineArgumentErrorCategory ErrorCategory - => CommandLineArgumentErrorCategory.MissingRequiredArgument; - - /// - /// Gets or sets a value that indicates whether this validator's help should be included - /// in the usage help. - /// - /// - /// to include it in the description; otherwise, . - /// The default value is . - /// - /// - /// - /// This has no effect if the - /// property is . - /// - /// - /// The help text is the value returned by . - /// - /// - public bool IncludeInUsageHelp { get; set; } = true; - - /// - /// Determines if the at least one of the arguments in was - /// supplied on the command line. - /// - /// The argument parser being validated. - /// - /// if the arguments are valid; otherwise, . - /// - public override bool IsValid(CommandLineParser parser) - => _arguments.Any(name => parser.GetArgument(name)?.HasValue ?? false); - - /// - public override string GetErrorMessage(CommandLineParser parser) - => parser.StringProvider.ValidateRequiresAnyFailed(GetArguments(parser)); - - /// - /// Gets the usage help message for this validator. - /// - /// The parser is the validator is for. - /// - /// The usage help message, or if the - /// property is . - /// - public override string? GetUsageHelp(CommandLineParser parser) - => IncludeInUsageHelp ? parser.StringProvider.RequiresAnyUsageHelp(GetArguments(parser)) : null; - - /// - /// Resolves the argument names in the property to their actual - /// property. - /// - /// The instance. - /// A list of the arguments. - /// - /// is . - /// - /// - /// One of the argument names in the property refers to an - /// argument that doesn't exist. - /// - public IEnumerable GetArguments(CommandLineParser parser) + if (_arguments.Length <= 1) { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } - - return _arguments - .Select(name => parser.GetArgument(name) ?? throw GetUnknownDependencyException(name)); + throw new ArgumentException(Properties.Resources.RequiresAnySingleArgument, nameof(arguments)); } - private InvalidOperationException GetUnknownDependencyException(string name) + _arguments = arguments; + } + + /// + /// Gets the names of the arguments, one of which must be supplied on the command line. + /// + /// + /// The names of the arguments. + /// + public string[] Arguments => _arguments; + + /// + /// Gets the error category used for the when + /// validation fails. + /// + /// + /// . + /// + public override CommandLineArgumentErrorCategory ErrorCategory + => CommandLineArgumentErrorCategory.MissingRequiredArgument; + + /// + /// Gets or sets a value that indicates whether this validator's help should be included + /// in the usage help. + /// + /// + /// to include it in the description; otherwise, . + /// The default value is . + /// + /// + /// + /// This has no effect if the + /// property is . + /// + /// + /// The help text is the value returned by . + /// + /// + public bool IncludeInUsageHelp { get; set; } = true; + + /// + /// Determines if at least one of the arguments in the property was + /// supplied on the command line. + /// + /// The command line parser being validated. + /// + /// if the arguments are valid; otherwise, . + /// + public override bool IsValid(CommandLineParser parser) + => _arguments.Any(name => parser.GetArgument(name)?.HasValue ?? false); + + /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + public override string GetErrorMessage(CommandLineParser parser) + => parser.StringProvider.ValidateRequiresAnyFailed(GetArguments(parser)); + + /// + /// Gets the usage help message for this validator. + /// + /// The command line parser that the validator is for. + /// + /// The usage help message, or if the + /// property is . + /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + public override string? GetUsageHelp(CommandLineParser parser) + => IncludeInUsageHelp ? parser.StringProvider.RequiresAnyUsageHelp(GetArguments(parser)) : null; + + /// + /// Resolves the argument names in the property to their actual + /// instances. + /// + /// The instance. + /// A list of the arguments. + /// + /// is . + /// + /// + /// One of the argument names in the property refers to an + /// argument that doesn't exist. + /// + public IEnumerable GetArguments(CommandLineParser parser) + { + if (parser == null) { - return new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.UnknownDependencyFormat, GetType().Name, name)); + throw new ArgumentNullException(nameof(parser)); } + + return _arguments + .Select(name => parser.GetArgument(name) ?? throw GetUnknownDependencyException(name)); + } + + private InvalidOperationException GetUnknownDependencyException(string name) + { + return new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.UnknownDependencyFormat, GetType().Name, name)); } } diff --git a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs index d874d806..284b0db1 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs @@ -1,67 +1,81 @@ using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that an argument can only be used together with other arguments. +/// +/// +/// +/// This attribute can be used to indicate that an argument can only be used in combination +/// with one or more other arguments. If one or more of the dependencies does not have +/// a value, validation will fail. +/// +/// +/// This validator will not be checked until all arguments have been parsed. +/// +/// +/// If validation fails, a is thrown with the +/// error category set to . +/// +/// +/// The names of the arguments that are dependencies are not validated when the attribute is +/// created. If one of the specified arguments does not exist, an exception is thrown during +/// validation. +/// +/// +/// +public class RequiresAttribute : DependencyValidationAttribute { /// - /// Validates that an argument can only be used together with other arguments. + /// Initializes a new instance of the class. + /// + /// The name of the argument that this argument depends on. + /// + /// is . + /// + public RequiresAttribute(string argument) + : base(true, argument) + { + } + + /// + /// Initializes a new instance of the class with multiple + /// dependencies. /// + /// The names of the arguments that this argument depends on. + /// + /// is . + /// + public RequiresAttribute(params string[] arguments) + : base(true, arguments) + { + } + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. /// /// - /// This attribute can be used to indicate that an argument can only be used in combination - /// with one or more other attributes. If one or more of the dependencies does not have - /// a value, validation will fail. - /// - /// - /// This validator will not be checked until all arguments have been parsed. - /// - /// - /// If validation fails, a is thrown with the - /// error category set to . + /// Use a custom class that overrides the + /// method + /// to customize this message. /// + /// + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateRequiresFailed(argument.MemberName, GetArguments(argument.Parser)); + + /// + /// /// - /// The names of the arguments that are dependencies are not validated when the attribute is - /// created. If one of the specified arguments does not exist, validation will always fail. + /// Use a custom class that overrides the + /// method + /// to customize this message. /// /// - /// - public class RequiresAttribute : DependencyValidationAttribute - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the argument that this argument depends on. - /// - /// is . - /// - public RequiresAttribute(string argument) - : base(true, argument) - { - } - - /// - /// Initializes a new instance of the class with multiple - /// dependencies. - /// - /// The names of the arguments that this argument depends on. - /// - /// is . - /// - public RequiresAttribute(params string[] arguments) - : base(true, arguments) - { - } - - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidateRequiresFailed(argument.MemberName, GetArguments(argument.Parser)); - - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.RequiresUsageHelp(GetArguments(argument.Parser)); - } + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.RequiresUsageHelp(GetArguments(argument.Parser)); } diff --git a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs index 2a1333dd..70526e7e 100644 --- a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs @@ -1,99 +1,112 @@ using System.Collections; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates whether the number of items for a multi-value or dictionary argument is in the +/// specified range. +/// +/// +/// +/// If the argument is optional and has no value, this validator will not be used, so zero +/// values is valid regardless of the lower bound specified. If you want zero values to be +/// invalid, make it a required argument. +/// +/// +/// This validator will not be checked until all arguments have been parsed. +/// +/// +/// If this validator is used on an argument that is not a multi-value or dictionary argument, +/// validation will always fail. +/// +/// +/// +public class ValidateCountAttribute : ArgumentValidationWithHelpAttribute { + private readonly int _minimum; + private readonly int _maximum; + /// - /// Validates whether the number of items for a multi-value or dictionary argument is in the - /// specified range. + /// Initializes a new instance of the class. /// - /// - /// - /// If the argument is optional and has no value, this validator will not be used, so no - /// values is valid regardless of the lower bound specified. If you want the argument to have - /// a value, make is a required argument. - /// - /// - /// This validator will not be checked until all arguments have been parsed. - /// - /// - /// If this validator is used on an argument that is not a multi-value or dictionary argument, - /// validation will always fail. - /// - /// - /// - public class ValidateCountAttribute : ArgumentValidationWithHelpAttribute + /// The inclusive lower bound on the number of elements. + /// The inclusive upper bound on the number of elements. + public ValidateCountAttribute(int minimum, int maximum = int.MaxValue) { - private readonly int _minimum; - private readonly int _maximum; - - /// - /// Initializes a new instance of the class. - /// - /// The inclusive lower bound on the number of elements. - /// The inclusive upper bound on the number of elements. - public ValidateCountAttribute(int minimum, int maximum = int.MaxValue) - { - _minimum = minimum; - _maximum = maximum; - } + _minimum = minimum; + _maximum = maximum; + } - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// . - /// - public override ValidationMode Mode => ValidationMode.AfterParsing; + /// + /// Gets a value that indicates when validation will run. + /// + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.AfterParsing; - /// - /// Gets the inclusive lower bound on the string length. - /// - /// - /// The inclusive lower bound on the string length. - /// - public int Minimum => _minimum; + /// + /// Gets the inclusive lower bound on the number of elements. + /// + /// + /// The inclusive lower bound on the number of elements. + /// + public int Minimum => _minimum; - /// - /// Get the inclusive upper bound on the string length. - /// - /// - /// The inclusive upper bound on the string length. - /// - public int Maximum => _maximum; + /// + /// Get the inclusive upper bound on the number of elements. + /// + /// + /// The inclusive upper bound on the number of elements. + /// + public int Maximum => _maximum; - /// - /// Determines if the argument's item count is in the range. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) + /// + /// Determines if the argument's item count is in the range. + /// + /// The argument being validated. + /// + /// The argument value. If not , this must be an instance of + /// . + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + if (argument.MultiValueInfo == null) { - if (!argument.IsMultiValue) - { - return false; - } - - var count = ((ICollection)argument.Value!).Count; - return count >= _minimum && count <= _maximum; + return false; } - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidateCountFailed(argument.ArgumentName, this); - - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ValidateCountUsageHelp(this); + var count = ((ICollection)argument.Value!).Count; + return count >= _minimum && count <= _maximum; } + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateCountFailed(argument.ArgumentName, this); + + /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateCountUsageHelp(this); } diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index 690c9c71..409a34ca 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -1,82 +1,97 @@ -using System; -using System.ComponentModel; +using Ookii.CommandLine.Conversion; +using System; using System.Globalization; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates whether the value of an enumeration type is one of the defined values for that +/// type. +/// +/// +/// +/// The used to convert values for arguments with enumeration types +/// allows conversion using the string representation of the underlying value, as well as the +/// name. While names are checked against the members, any underlying value can be converted to an +/// enumeration, regardless of whether it's a defined value for the enumeration. +/// +/// +/// For example, using the enumeration, converting a string value of +/// "9" would result in a value of (DayOfWeek)9, even though there is no enumeration +/// member with that value. +/// +/// +/// This validator makes sure that the result of conversion is a valid value for the +/// enumeration, by using the method. +/// +/// +/// 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 +/// . +/// +/// +/// It is an error to use this validator on an argument whose type is not an enumeration. +/// +/// +/// +public class ValidateEnumValueAttribute : ArgumentValidationWithHelpAttribute { + /// + /// Determines if the argument's value is defined. + /// + /// is not an argument with an enumeration type. + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + if (!argument.ElementType.IsEnum) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, + Properties.Resources.ArgumentNotEnumFormat, argument.ArgumentName)); + } + + return value == null || argument.ElementType.IsEnumDefined(value); + } + /// - /// Validates whether the value of an enumeration type is one of the defined values for that - /// type. + /// 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 class, which is the default - /// for enumerations, allows conversion using the string representation of the underlying - /// value, as well as the name. While names are checked against the members, any underlying - /// value can be converted to an enumeration, regardless of whether it's a defined value for - /// the enumeration. - /// - /// - /// For example, using the enumeration, converting a string value of - /// "9" would result in a value of (DayOfWeek)9, even though there is no enumeration - /// member with that value. - /// - /// - /// This validator makes sure that the result of conversion is a valid value for the - /// enumeration, by using the method. + /// 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; } + + /// + /// /// - /// 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 - /// . + /// Use a custom class that overrides the + /// method + /// to customize this message. /// + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateEnumValueUsageHelp(argument.ElementType); + + /// + /// /// - /// It is an error to use this validator on an argument whose type is not an enumeration. + /// Use a custom class that overrides the + /// method + /// to customize this message. /// /// - public class ValidateEnumValueAttribute : ArgumentValidationWithHelpAttribute - { - /// - /// Determines if the argument's value is defined. - /// - /// is not an argument with an enumeration type. - /// - public override bool IsValid(CommandLineArgument argument, object? value) - { - if (!argument.ElementType.IsEnum) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, - Properties.Resources.ArgumentNotEnumFormat, argument.ArgumentName)); - } - - return 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 error message is only used if the validation fails, which only the case for - /// undefined numerical values. Strings that don't match the name don't use this error. - /// - /// - public bool IncludeValuesInErrorMessage { get; set; } - - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ValidateEnumValueUsageHelp(argument.ElementType); - - /// - public override string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, argument.ElementType, value, - IncludeValuesInErrorMessage); - } + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, argument.ElementType, value, + IncludeValuesInErrorMessage); } diff --git a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs index 3b6bf2e7..1d1f4cf2 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs @@ -1,66 +1,70 @@ -namespace Ookii.CommandLine.Validation +using System; + +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that the value of an argument is not an empty string. +/// +/// +/// +/// This validator uses the raw string value provided by the user, before type conversion takes +/// place. +/// +/// +/// If the argument is optional, validation is only performed if the argument is specified, +/// so the value may still be an empty string if the argument is not supplied, if that +/// is the default value. +/// +/// +/// +public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute { /// - /// Validates that the value of an argument is not an empty string. + /// Gets a value that indicates when validation will run. /// - /// - /// - /// If the argument's type is not , this validator uses the raw string - /// value provided by the user, before type conversion takes place. - /// - /// - /// If the argument is optional, validation is only performed if the argument is specified, - /// so the value may still be if the argument is not supplied, if that - /// is the default value. - /// - /// - /// - public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.BeforeConversion; + + /// + /// Determines if the argument is not an empty string. + /// + /// The argument being validated. + /// + /// The raw string argument value provided by the user on the command line. + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) { - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// . - /// - public override ValidationMode Mode => ValidationMode.BeforeConversion; + return !string.IsNullOrEmpty(value as string); + } + + /// + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + => !value.IsEmpty; - /// - /// Determines if the argument's value is not null or empty. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. + public override string GetErrorMessage(CommandLineArgument argument, object? value) + { + if (value == null) { - return !string.IsNullOrEmpty(value as string); + return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); } - - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) + else { - if (value == null) - { - return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); - } - else - { - return argument.Parser.StringProvider.ValidateNotEmptyFailed(argument.ArgumentName); - } + return argument.Parser.StringProvider.ValidateNotEmptyFailed(argument.ArgumentName); } - - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ValidateNotEmptyUsageHelp(); } + + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateNotEmptyUsageHelp(); } diff --git a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs index 6a0ceff1..7d40a6fa 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs @@ -1,60 +1,66 @@ -using System; +using Ookii.CommandLine.Conversion; +using System; using System.ComponentModel; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that the value of an argument is not . +/// +/// +/// +/// An argument's value can only be if its +/// returns from the +/// +/// method. For example, the can return . +/// +/// +/// It is not necessary to use this attribute on required arguments with types that can't be +/// , such as value types (except ), and if +/// using .Net 6.0 or later, non-nullable reference types. The +/// already ensures it will not assign to these arguments. +/// +/// +/// If the argument is optional, validation is only performed if the argument is specified, +/// so the value may still be if the argument is not supplied, if that +/// is the default value. +/// +/// +/// This validator does not add any help text to the argument description. +/// +/// +/// +public class ValidateNotNullAttribute : ArgumentValidationAttribute { /// - /// Validates that the value of an argument is not . + /// Determines if the argument's value is not . + /// + /// The argument being validated. + /// + /// The argument value. If not , this must be an instance of + /// . + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + return value != null; + } + + /// + /// Gets the error message to display if validation failed. /// + /// The argument that was validated. + /// Not used. + /// The error message. /// /// - /// An argument's value can only be if its - /// returns from the - /// method. For example, the can return . - /// - /// - /// It is not necessary to use this attribute on required arguments with types that can't be - /// , such as value types (except , and if - /// using .Net 6.0 or later, non-nullable reference types. The - /// already ensures it will not assign to these arguments. - /// - /// - /// If the argument is optional, validation is only performed if the argument is specified, - /// so the value may still be if the argument is not supplied, if that - /// is the default value. - /// - /// - /// This validator does not add any help text to the argument description. + /// Use a custom class that overrides the + /// method + /// to customize this message. /// /// - /// - public class ValidateNotNullAttribute : ArgumentValidationAttribute - { - /// - /// Determines if the argument's value is not null. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) - { - return value != null; - } - - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) - { - return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); - } - } + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); } diff --git a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs index 9c78bc1b..84cc0575 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs @@ -1,69 +1,88 @@ -namespace Ookii.CommandLine.Validation +using System; + +namespace Ookii.CommandLine.Validation; + + +/// +/// Validates that the value of an argument is not an empty string, or a string containing only +/// white-space characters. +/// +/// +/// +/// This validator uses the raw string value provided by the user, before type conversion takes +/// place. +/// +/// +/// If the argument is optional, validation is only performed if the argument is specified, +/// so the value may still be an empty or white-space-only string if the argument is not supplied, +/// if that is the default value. +/// +/// +/// +public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribute { + /// + /// Gets a value that indicates when validation will run. + /// + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.BeforeConversion; /// - /// Validates that the value of an argument is not an empty string, or a string containing only - /// white-space characters. + /// Determines if the argument's value is not an empty string, or contains only white-space + /// characters. /// + /// The argument being validated. + /// + /// The raw string argument value. + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + return !string.IsNullOrWhiteSpace(value as string); + } + + /// + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + => !value.IsWhiteSpace(); + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. /// - /// - /// If the argument's type is not , this validator uses the raw string - /// value provided by the user, before type conversion takes place. - /// /// - /// If the argument is optional, validation is only performed if the argument is specified, - /// so the value may still be if the argument is not supplied, if that - /// is the default value. + /// Use a custom class that overrides the + /// + /// method to customize this message. /// /// - /// - public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribute + public override string GetErrorMessage(CommandLineArgument argument, object? value) { - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// . - /// - public override ValidationMode Mode => ValidationMode.BeforeConversion; - - /// - /// Determines if the argument's value is not null or only white-space characters. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) + if (value == null) { - return !string.IsNullOrWhiteSpace(value as string); + return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); } - - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) + else { - if (value == null) - { - return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); - } - else - { - return argument.Parser.StringProvider.ValidateNotWhiteSpaceFailed(argument.ArgumentName); - } + return argument.Parser.StringProvider.ValidateNotWhiteSpaceFailed(argument.ArgumentName); } + } - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ValidateNotWhiteSpaceUsageHelp(); + /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateNotWhiteSpaceUsageHelp(); - } } diff --git a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs index e77886c6..5732846d 100644 --- a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs @@ -1,117 +1,150 @@ -using System.Globalization; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text.RegularExpressions; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that an argument's value matches the specified regular expression. +/// +/// +/// +/// This validator uses the raw string value provided by the user, before type conversion takes +/// place. +/// +/// +/// This validator does not add any help text to the argument description. +/// +/// +/// +/// +public class ValidatePatternAttribute : ArgumentValidationAttribute { + private readonly string _pattern; + private Regex? _patternRegex; + private readonly RegexOptions _options; + /// - /// Validates that an argument's value matches the specified . + /// Initializes a new instance of the class. /// + /// The regular expression to match against. + /// A combination of values to use. /// - /// - /// If the argument's type is not , this validator uses the raw string - /// value provided by the user, before type conversion takes place. - /// /// - /// This validator does not add any help text to the argument description. + /// This constructor does not validate if the regular expression specified in + /// is valid. The instance is not constructed until the validation + /// is performed. /// /// - /// - public class ValidatePatternAttribute : ArgumentValidationAttribute + public ValidatePatternAttribute( +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Regex, nameof(options))] +#endif + string pattern, RegexOptions options = RegexOptions.None) { - private readonly string _pattern; - private Regex? _patternRegex; - private readonly RegexOptions _options; + _pattern = pattern ?? throw new ArgumentNullException(nameof(pattern)); + _options = options; + } - /// - /// Initializes a new instance of the class. - /// - /// The regular expression to match against. - /// A combination of values to use. - /// - /// - /// This constructor does not validate if the regular expression specified in - /// is valid. The instance is not constructed until the validation - /// is performed. - /// - /// - public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOptions.None) - { - _pattern = pattern; - _options = options; - } + /// + /// Gets a value that indicates when validation will run. + /// + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.BeforeConversion; - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// . - /// - public override ValidationMode Mode => ValidationMode.BeforeConversion; + /// + /// Gets or sets a custom error message to use. + /// + /// + /// A compound format string for the error message to use, or to + /// use a generic error message. + /// + /// + /// + /// If this property is , the message returned by + /// will be used. + /// + /// + /// This property is a compound format string, and may have three placeholders: + /// {0} for the argument name, {1} for the value, and {2} for the pattern. + /// + /// +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.CompositeFormat)] +#endif + public string? ErrorMessage { get; set; } - /// - /// Gets or sets a custom error message to use. - /// - /// - /// A compound format string for the error message to use, or to - /// use a generic error message. - /// - /// - /// - /// If this property is , the message returned by - /// will be used. - /// - /// - /// This property is a compound format string, and may have three placeholders: - /// {0} for the argument name, {1} for the value, and {2} for the pattern. - /// - /// - public string? ErrorMessage { get; set; } + /// + /// Gets the regular expression that values must match. + /// + /// + /// The pattern that values must match. + /// + public virtual Regex Pattern => _patternRegex ??= new Regex(_pattern, _options); - /// - /// Gets the pattern that values must match. - /// - /// - /// The pattern that values must match. - /// - public Regex Pattern => _patternRegex ??= new Regex(_pattern, _options); + /// + /// Gets the regular expression string stored in this attribute. + /// + /// + /// The regular expression. + /// + public string PatternValue => _pattern; - /// - /// Determines if the argument's value matches the pattern. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) + /// + /// Determines if the argument's value matches the pattern. + /// + /// The argument being validated. + /// + /// The raw string argument value. + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + if (value is not string stringValue) { - if (value is not string stringValue) - { - return false; - } - - return Pattern.IsMatch(stringValue); + return false; } - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The value of the property, or a generic message - /// if it's . - public override string GetErrorMessage(CommandLineArgument argument, object? value) - { - if (ErrorMessage == null) - { - return base.GetErrorMessage(argument, value); - } + return Pattern.IsMatch(stringValue); + } - return string.Format(CultureInfo.CurrentCulture, ErrorMessage, argument.ArgumentName, value, _pattern); +#if NET7_0_OR_GREATER + + /// + /// Determines if the argument's value matches the pattern. + /// + /// The argument being validated. + /// + /// The raw string argument value. + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + => Pattern.IsMatch(value); + +#endif + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The value of the property, or a generic message + /// if it's . + public override string GetErrorMessage(CommandLineArgument argument, object? value) + { + if (ErrorMessage == null) + { + return base.GetErrorMessage(argument, value); } + + return string.Format(CultureInfo.CurrentCulture, ErrorMessage, argument.ArgumentName, value, _pattern); } } diff --git a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs index 58235bb0..e4b7e62a 100644 --- a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs @@ -1,112 +1,123 @@ using System; -using System.ComponentModel; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates whether an argument value is in the specified range. +/// +/// +/// +/// This attribute can only be used with argument's whose type implements . +/// +/// +/// +public class ValidateRangeAttribute : ArgumentValidationWithHelpAttribute { + private readonly object? _minimum; + private readonly object? _maximum; + /// - /// Validates whether an argument value is in the specified range. + /// Initializes a new instance of the class. /// + /// + /// The inclusive lower bound of the range, or if + /// the range has no lower bound. + /// + /// + /// The inclusive upper bound of the range, or if + /// the range has no upper bound. + /// /// /// - /// This attribute can only be used with argument's whose type implements . + /// When not , both and + /// must be an instance of the argument type, or a string. /// /// - /// - public class ValidateRangeAttribute : ArgumentValidationWithHelpAttribute + /// + /// and are both . + /// + public ValidateRangeAttribute(object? minimum, object? maximum) { - private readonly object? _minimum; - private readonly object? _maximum; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The inclusive lower bound of the range, or if - /// the range has no lower bound. - /// - /// - /// The inclusive upper bound of the range, or if - /// the range has no upper bound. - /// - /// - /// - /// When not , both and - /// must be an instance of the argument type, or a type that can be converted to the - /// argument type using its . - /// - /// - /// - /// and are both . - /// - public ValidateRangeAttribute(object? minimum, object? maximum) + if (minimum == null && maximum == null) { - if (minimum == null && maximum == null) - { - throw new ArgumentException(Properties.Resources.MinMaxBothNull); - } - - _minimum = minimum; - _maximum = maximum; + throw new ArgumentException(Properties.Resources.MinMaxBothNull); } - /// - /// Gets the inclusive lower bound of the range. - /// - /// - /// The inclusive lower bound of the range, or if - /// the range has no lower bound. - /// - public virtual object? Minimum => _minimum; - - /// - /// Gets the inclusive upper bound of the range. - /// - /// - /// The inclusive upper bound of the range, or if - /// the range has no upper bound. - /// - public virtual object? Maximum => _maximum; + _minimum = minimum; + _maximum = maximum; + } - /// - /// Determines if the argument's value is in the range. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) - { - var min = (IComparable?)argument.ConvertToArgumentTypeInvariant(Minimum); - var max = (IComparable?)argument.ConvertToArgumentTypeInvariant(Maximum); + /// + /// Gets the inclusive lower bound of the range. + /// + /// + /// The inclusive lower bound of the range, or if + /// the range has no lower bound. + /// + public virtual object? Minimum => _minimum; - if (min != null && min.CompareTo(value) > 0) - { - return false; - } + /// + /// Gets the inclusive upper bound of the range. + /// + /// + /// The inclusive upper bound of the range, or if + /// the range has no upper bound. + /// + public virtual object? Maximum => _maximum; - if (max != null && max.CompareTo(value) < 0) - { - return false; - } + /// + /// Determines if the argument's value is in the range. + /// + /// The argument being validated. + /// + /// The argument value. If not , this must be an instance of + /// . + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + var min = (IComparable?)argument.ConvertToArgumentTypeInvariant(Minimum); + var max = (IComparable?)argument.ConvertToArgumentTypeInvariant(Maximum); - return true; + if (min != null && min.CompareTo(value) > 0) + { + return false; } - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidateRangeFailed(argument.ArgumentName, this); + if (max != null && max.CompareTo(value) < 0) + { + return false; + } - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ValidateRangeUsageHelp(this); + return true; } + + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateRangeFailed(argument.ArgumentName, this); + + /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateRangeUsageHelp(this); } diff --git a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs index e95c96af..3f707ec8 100644 --- a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs @@ -1,83 +1,104 @@ -namespace Ookii.CommandLine.Validation +using System; + +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that the string length of an argument's value is in the specified range. +/// +/// +/// +/// This validator uses the raw string value provided by the user, before type conversion takes +/// place. +/// +/// +/// +public class ValidateStringLengthAttribute : ArgumentValidationWithHelpAttribute { + private readonly int _minimum; + private readonly int _maximum; + /// - /// Validates that the string length of an argument's value is in the specified range. + /// Initializes a new instance of the class. /// - /// - /// - /// If the argument's type is not , this validator uses the raw string - /// value provided by the user, before type conversion takes place. - /// - /// - /// - public class ValidateStringLengthAttribute : ArgumentValidationWithHelpAttribute + /// The inclusive lower bound on the length. + /// The inclusive upper bound on the length. + public ValidateStringLengthAttribute(int minimum, int maximum = int.MaxValue) { - private readonly int _minimum; - private readonly int _maximum; + _minimum = minimum; + _maximum = maximum; + } - /// - /// Initializes a new instance of the class. - /// - /// The inclusive lower bound on the length. - /// The inclusive upper bound on the length. - public ValidateStringLengthAttribute(int minimum, int maximum = int.MaxValue) - { - _minimum = minimum; - _maximum = maximum; - } + /// + /// Gets a value that indicates when validation will run. + /// + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.BeforeConversion; - /// - /// Gets a value that indicates when validation will run. - /// - /// - /// . - /// - public override ValidationMode Mode => ValidationMode.BeforeConversion; + /// + /// Gets the inclusive lower bound on the string length. + /// + /// + /// The inclusive lower bound on the string length. + /// + public int Minimum => _minimum; - /// - /// Gets the inclusive lower bound on the string length. - /// - /// - /// The inclusive lower bound on the string length. - /// - public int Minimum => _minimum; + /// + /// Get the inclusive upper bound on the string length. + /// + /// + /// The inclusive upper bound on the string length. + /// + public int Maximum => _maximum; - /// - /// Get the inclusive upper bound on the string length. - /// - /// - /// The inclusive upper bound on the string length. - /// - public int Maximum => _maximum; + /// + /// Determines if the argument's value's length is in the range. + /// + /// The argument being validated. + /// + /// The raw string value of the argument. + /// + /// + /// if the value is valid; otherwise, . + /// + public override bool IsValid(CommandLineArgument argument, object? value) + { + var length = (value as string)?.Length ?? 0; + return length >= _minimum && length <= _maximum; + } - /// - /// Determines if the argument's value's length is in the range. - /// - /// The argument being validated. - /// - /// The argument value. If not , this must be an instance of - /// . - /// - /// - /// if the value is valid; otherwise, . - /// - public override bool IsValid(CommandLineArgument argument, object? value) - { - var length = (value as string)?.Length ?? 0; - return length >= _minimum && length <= _maximum; - } + /// + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + { + var length = value.Length; + return length >= _minimum && length <= _maximum; + } - /// - /// Gets the error message to display if validation failed. - /// - /// The argument that was validated. - /// Not used. - /// The error message. - public override string GetErrorMessage(CommandLineArgument argument, object? value) - => argument.Parser.StringProvider.ValidateStringLengthFailed(argument.ArgumentName, this); + /// + /// Gets the error message to display if validation failed. + /// + /// The argument that was validated. + /// Not used. + /// The error message. + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateStringLengthFailed(argument.ArgumentName, this); - /// - protected override string GetUsageHelpCore(CommandLineArgument argument) - => argument.Parser.StringProvider.ValidateStringLengthUsageHelp(this); - } + /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateStringLengthUsageHelp(this); } diff --git a/src/Ookii.CommandLine/Validation/ValidationMode.cs b/src/Ookii.CommandLine/Validation/ValidationMode.cs index e0c797fb..a62679cf 100644 --- a/src/Ookii.CommandLine/Validation/ValidationMode.cs +++ b/src/Ookii.CommandLine/Validation/ValidationMode.cs @@ -1,28 +1,27 @@ -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Specifies when a class that derives from the class +/// will run validation. +/// +public enum ValidationMode { /// - /// Specifies when a derived class of the class - /// will run validation. + /// Validation will occur after the value is converted. The value passed to + /// the method is an instance of the + /// argument's type. /// - public enum ValidationMode - { - /// - /// Validation will occur after the value is converted. The value passed to - /// the method is an instance of the - /// argument's type. - /// - AfterConversion, - /// - /// Validation will occur before the value is converted. The value passed to - /// the method is the raw string provided - /// by the user, and is not yet set. - /// - BeforeConversion, - /// - /// Validation will occur after all arguments have been parsed. Validators will only be - /// called on arguments with values, and the value passed to - /// is always . - /// - AfterParsing, - } + AfterConversion, + /// + /// Validation will occur before the value is converted. The value passed to + /// the method is the raw string provided + /// by the user, and is not yet set. + /// + BeforeConversion, + /// + /// Validation will occur after all arguments have been parsed. Validators will only be + /// called on arguments with values, and the value passed to + /// is always . + /// + AfterParsing, } diff --git a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs index 8d823b7a..95b9ec15 100644 --- a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs +++ b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs @@ -1,64 +1,76 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; using System.ComponentModel; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Supplies a short description of the arguments's value to use when printing usage information. +/// +/// +/// +/// The value description is a short, typically one-word description that indicates the +/// type of value that the user should supply. +/// +/// +/// If this attribute is not present, it is retrieved from the +/// property. If not found there, the type of the argument is used, applying the specified by the +/// property or the property. If +/// this is a multi-value argument, the element type is used. If the type is , +/// its underlying type is used. +/// +/// +/// If you want to override the value description for all arguments of a specific type, +/// use the property. +/// +/// +/// The value description is used when generating usage help. For example, the usage for an +/// argument named Sample with a value description of String would look like "-Sample <String>". +/// +/// +/// You can derive from this attribute to use an alternative source for the value description, +/// such as a resource table that can be localized. +/// +/// +/// This is not the long description used to describe the purpose of the argument. That can be set +/// using the attribute. +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +public class ValueDescriptionAttribute : Attribute { /// - /// Provides a custom value description for use in the usage help for an argument created from a constructor parameter. + /// Initializes a new instance of the attribute. + /// + /// The value description for the argument. + /// + /// is . + /// + public ValueDescriptionAttribute(string valueDescription) + { + ValueDescriptionValue = valueDescription ?? throw new ArgumentNullException(nameof(valueDescription)); + } + + /// + /// Gets the value description for the argument. /// + /// + /// The value description. + /// + public virtual string ValueDescription => ValueDescriptionValue; + + /// + /// Gets the value description stored in this attribute. + /// + /// + /// The value description. + /// /// /// - /// The value description is a short, typically one-word description that indicates the - /// type of value that the user should supply. + /// The default implementation of the property returns the + /// value of this property. /// - /// - /// If not specified here, it is retrieved from the - /// property, and if not found there, the type of the property is used, applying the - /// specified by the - /// property or the property. - /// If this is a multi-value argument, the element type is used. If the type is , - /// its underlying type is used. - /// - /// - /// If you want to override the value description for all arguments of a specific type, - /// use the property. - /// - /// - /// The value description is used when printing usage. For example, the usage for an argument named Sample with - /// a value description of String would look like "-Sample <String>". - /// - /// - /// This is not the long description used to describe the purpose of the argument. That should be specified - /// using the attribute. - /// /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class ValueDescriptionAttribute : Attribute - { - private readonly string _valueDescription; - - /// - /// Initializes a new instance of the class. - /// - /// The custom value description. - /// - /// is . - /// - public ValueDescriptionAttribute(string valueDescription) - { - _valueDescription = valueDescription ?? throw new ArgumentNullException(nameof(valueDescription)); - } - - /// - /// Gets the custom value description. - /// - /// - /// The custom value description. - /// - public string ValueDescription - { - get { return _valueDescription; } - } - } + protected string ValueDescriptionValue { get; } } diff --git a/src/Ookii.CommandLine/ValueTypeConverterAttribute.cs b/src/Ookii.CommandLine/ValueTypeConverterAttribute.cs deleted file mode 100644 index 1f90e868..00000000 --- a/src/Ookii.CommandLine/ValueTypeConverterAttribute.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; -using System.Collections.Generic; -using System.ComponentModel; - -namespace Ookii.CommandLine -{ - /// - /// Specifies a to use for the values of a dictionary argument. - /// - /// - /// - /// This attribute can be used along with the - /// attribute to customize the parsing of a dictionary argument without having to write a - /// custom that returns a . - /// - /// - /// This attribute is ignored if the argument uses the - /// attribute or if the argument is not a dictionary argument. - /// - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public class ValueTypeConverterAttribute : Attribute - { - private readonly string _converterTypeName; - - /// - /// Initializes a new instance of the class. - /// - /// The type of the custom to use. - /// is . - public ValueTypeConverterAttribute(Type converterType) - { - _converterTypeName = converterType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(converterType)); - } - - /// - /// Initializes a new instance of the class. - /// - /// The type name of the custom to use. - /// is . - public ValueTypeConverterAttribute(string converterTypeName) - { - _converterTypeName = converterTypeName ?? throw new ArgumentNullException(nameof(converterTypeName)); - } - - /// - /// Gets the type of the custom to use. - /// - public string ConverterTypeName => _converterTypeName; - } -} diff --git a/src/Ookii.CommandLine/WrappingMode.cs b/src/Ookii.CommandLine/WrappingMode.cs index 21b9f9bd..41aeb907 100644 --- a/src/Ookii.CommandLine/WrappingMode.cs +++ b/src/Ookii.CommandLine/WrappingMode.cs @@ -1,25 +1,24 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates how the class will wrap text at the maximum +/// line length. +/// +/// +public enum WrappingMode { /// - /// Indicates how the class will wrap text at the maximum - /// line length. + /// The text will not be wrapped at the maximum line length. /// - /// - public enum WrappingMode - { - /// - /// The text will not be wrapped at the maximum line length. - /// - Disabled, - /// - /// The text will be white-space wrapped at the maximum line length, and if there is no - /// suitable white-space location to wrap the text, it will be wrapped at the line length. - /// - Enabled, - /// - /// The text will be white-space wrapped at the maximum line length. If there is no suitable - /// white-space location to wrap the text, the line will not be wrapped. - /// - EnabledNoForce - } + Disabled, + /// + /// The text will be white-space wrapped at the maximum line length, and if there is no + /// suitable white-space location to wrap the text, it will be wrapped at the line length. + /// + Enabled, + /// + /// The text will be white-space wrapped at the maximum line length. If there is no suitable + /// white-space location to wrap the text, the line will not be wrapped. + /// + EnabledNoForce } diff --git a/src/Samples/ArgumentDependencies/ArgumentDependencies.csproj b/src/Samples/ArgumentDependencies/ArgumentDependencies.csproj index 4b423c05..c637ecb6 100644 --- a/src/Samples/ArgumentDependencies/ArgumentDependencies.csproj +++ b/src/Samples/ArgumentDependencies/ArgumentDependencies.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 enable enable Command line parsing sample for Ookii.CommandLine, using argument dependencies. @@ -10,8 +10,15 @@ This is sample code, so you can use it freely. + + diff --git a/src/Samples/ArgumentDependencies/Program.cs b/src/Samples/ArgumentDependencies/Program.cs index 37f1c0be..d3f52d4d 100644 --- a/src/Samples/ArgumentDependencies/Program.cs +++ b/src/Samples/ArgumentDependencies/Program.cs @@ -1,28 +1,21 @@ -using Ookii.CommandLine; +using ArgumentDependencies; +using Ookii.CommandLine; -namespace ArgumentDependencies; - -static class Program +var arguments = ProgramArguments.Parse(); +if (arguments == null) { - public static int Main() - { - var arguments = ProgramArguments.Parse(); - if (arguments == null) - { - return 1; - } - - using var writer = LineWrappingTextWriter.ForConsoleOut(); - if (arguments.Path != null) - { - writer.WriteLine($"Path: {arguments.Path.FullName}"); - } - else - { - writer.WriteLine($"IP address: {arguments.Ip}"); - writer.WriteLine($"Port: {arguments.Port}"); - } + return 1; +} - return 0; - } +using var writer = LineWrappingTextWriter.ForConsoleOut(); +if (arguments.Path != null) +{ + writer.WriteLine($"Path: {arguments.Path.FullName}"); } +else +{ + writer.WriteLine($"IP address: {arguments.Ip}"); + writer.WriteLine($"Port: {arguments.Port}"); +} + +return 0; diff --git a/src/Samples/ArgumentDependencies/ProgramArguments.cs b/src/Samples/ArgumentDependencies/ProgramArguments.cs index 338e726c..f4343eb1 100644 --- a/src/Samples/ArgumentDependencies/ProgramArguments.cs +++ b/src/Samples/ArgumentDependencies/ProgramArguments.cs @@ -19,12 +19,13 @@ namespace ArgumentDependencies; // // If you use a NameTransform that changes the argument names, or use any explicit argument // names, you CANNOT use nameof()! +[GeneratedParser] [ApplicationFriendlyName("Ookii.CommandLine Dependency Sample")] [Description("Sample command line application with argument dependencies. The application parses the command line and prints the results, but otherwise does nothing and none of the arguments are actually used for anything.")] [RequiresAny(nameof(Path), nameof(Ip))] -internal class ProgramArguments +partial class ProgramArguments { - [CommandLineArgument(Position = 0)] + [CommandLineArgument(IsPositional = true)] [Description("The path to use.")] public FileInfo? Path { get; set; } @@ -36,13 +37,8 @@ internal class ProgramArguments // This argument uses the RequiresAttribute to indicate it can only be used if "-Ip" is also // specified. - [CommandLineArgument(DefaultValue = 80)] + [CommandLineArgument] [Description("The port to connect to.")] [Requires(nameof(Ip))] - public int Port { get; set; } - - public static ProgramArguments? Parse() - { - return CommandLineParser.Parse(); - } + public int Port { get; set; } = 80; } diff --git a/src/Samples/ArgumentDependencies/README.md b/src/Samples/ArgumentDependencies/README.md index fccc1d97..ee7205ee 100644 --- a/src/Samples/ArgumentDependencies/README.md +++ b/src/Samples/ArgumentDependencies/README.md @@ -1,9 +1,9 @@ # Argument dependencies sample -This sample shows how to use the argument dependency validators. These validators let you specify -that certain arguments must or cannot be used together. It also makes it possible to specify that -the user must use one of a set of arguments, something that can't be expressed with regular -required arguments. +This sample shows how to use the [argument dependency validators](../../../docs/Validation.md#argument-dependencies-and-restrictions). +These validators let you specify that certain arguments must or cannot be used together. It also +makes it possible to specify that the user must use one of a set of arguments, something that can't +be expressed with regular required arguments. The validators in question are the [`RequiresAttribute`][], the [`ProhibitsAttribute`][], and the [`RequiresAnyAttribute`][]. You can see them in action in @@ -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-3.1/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm -[`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm -[`RequiresAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm -[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm +[`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 diff --git a/src/Samples/CustomUsage/CustomUsage.csproj b/src/Samples/CustomUsage/CustomUsage.csproj index d042c721..6b7bcd86 100644 --- a/src/Samples/CustomUsage/CustomUsage.csproj +++ b/src/Samples/CustomUsage/CustomUsage.csproj @@ -1,8 +1,8 @@ - + Exe - net6.0 + net7.0 enable enable Command line parsing sample for Ookii.CommandLine, using customized usage help. @@ -10,8 +10,15 @@ This is sample code, so you can use it freely. + + diff --git a/src/Samples/CustomUsage/CustomUsageWriter.cs b/src/Samples/CustomUsage/CustomUsageWriter.cs index 57dc24f6..ed3f9111 100644 --- a/src/Samples/CustomUsage/CustomUsageWriter.cs +++ b/src/Samples/CustomUsage/CustomUsageWriter.cs @@ -102,21 +102,15 @@ protected override void WriteArgumentDescriptionHeader(CommandLineArgument argum names = names.Append(argument.ShortNameWithPrefix!); } - if (argument.ShortAliases != null) - { - var shortPrefix = argument.Parser.ArgumentNamePrefixes[0]; - names = names.Concat(argument.ShortAliases.Select(alias => shortPrefix + alias)); - } + var shortPrefix = argument.Parser.ArgumentNamePrefixes[0]; + names = names.Concat(argument.ShortAliases.Select(alias => shortPrefix + alias)); if (argument.HasLongName) { names = names.Append(argument.LongNameWithPrefix!); } - if (argument.Aliases != null) - { - names = names.Concat(argument.Aliases.Select(alias => argument.Parser.LongArgumentNamePrefix + alias)); - } + names = names.Concat(argument.Aliases.Select(alias => argument.Parser.LongArgumentNamePrefix + alias)); // Join up all the names. string name = string.Join('|', names); @@ -156,12 +150,9 @@ private static int CalculateNamesLength(CommandLineArgument argument) length += argument.ShortNameWithPrefix!.Length + 1; } - if (argument.ShortAliases != null) - { - var shortPrefixLength = argument.Parser.ArgumentNamePrefixes[0].Length; - // Space for prefix, short name, separator. - length += argument.ShortAliases.Count * (shortPrefixLength + 1 + 1); - } + var shortPrefixLength = argument.Parser.ArgumentNamePrefixes[0].Length; + // Space for prefix, short name, separator. + length += argument.ShortAliases.Length * (shortPrefixLength + 1 + 1); if (argument.HasLongName) { @@ -169,12 +160,9 @@ private static int CalculateNamesLength(CommandLineArgument argument) length += argument.LongNameWithPrefix!.Length + 1; } - if (argument.Aliases != null) - { - var longPrefixLength = argument.Parser.LongArgumentNamePrefix!.Length; - // Space for prefix, long name, separator. - length += argument.Aliases.Sum(alias => longPrefixLength + alias.Length + 1); - } + var longPrefixLength = argument.Parser.LongArgumentNamePrefix!.Length; + // Space for prefix, long name, separator. + length += argument.Aliases.Sum(alias => longPrefixLength + alias.Length + 1); // There is one separator too many length -= 1; diff --git a/src/Samples/CustomUsage/Program.cs b/src/Samples/CustomUsage/Program.cs index b88688c2..2737f4f9 100644 --- a/src/Samples/CustomUsage/Program.cs +++ b/src/Samples/CustomUsage/Program.cs @@ -1,9 +1,21 @@ -// Ookii.CommandLine is easy to use with top-level statements too. -using CustomUsage; +using CustomUsage; using Ookii.CommandLine; -// Parse the arguments. See ProgramArguments.cs for the definitions. -var arguments = ProgramArguments.Parse(); +// Not all options can be set with the ParseOptionsAttribute. +var options = new ParseOptions() +{ + // Set the value description of all int arguments to "number", instead of doing it + // separately on each argument. + DefaultValueDescriptions = new Dictionary() + { + { typeof(int), "number" } + }, + // Use our own string provider and usage writer for the custom usage strings. + StringProvider = new CustomStringProvider(), + UsageWriter = new CustomUsageWriter(), +}; + +var arguments = ProgramArguments.Parse(options); // No need to do anything when the value is null; Parse() already printed errors and // usage to the console. We return a non-zero value to indicate failure. diff --git a/src/Samples/CustomUsage/ProgramArguments.cs b/src/Samples/CustomUsage/ProgramArguments.cs index 6b3e7af8..f2679a48 100644 --- a/src/Samples/CustomUsage/ProgramArguments.cs +++ b/src/Samples/CustomUsage/ProgramArguments.cs @@ -10,26 +10,23 @@ namespace CustomUsage; // This sample sets the mode, case sensitivity and name transform to use POSIX conventions. // // See the Parse() method below to see how the usage customization is applied. +[GeneratedParser] [ApplicationFriendlyName("Ookii.CommandLine Long/Short Mode Sample")] [Description("Sample command line application with highly customized usage help. The application parses the command line and prints the results, but otherwise does nothing and none of the arguments are actually used for anything.")] -[ParseOptions(Mode = ParsingMode.LongShort, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, - CaseSensitive = true, - DuplicateArguments = ErrorMode.Warning)] -class ProgramArguments +[ParseOptions(IsPosix = true, DuplicateArguments = ErrorMode.Warning)] +partial class ProgramArguments { - [CommandLineArgument(Position = 0, IsRequired = true, IsShort = true)] + [CommandLineArgument(IsPositional = true, IsShort = true)] [Description("The source data.")] - public string? Source { get; set; } + public required string Source { get; set; } - [CommandLineArgument(Position = 1, IsRequired = true, IsShort = true)] + [CommandLineArgument(IsPositional = true, IsShort = true)] [Description("The destination data.")] - public string? Destination { get; set; } + public required string Destination { get; set; } - [CommandLineArgument(DefaultValue = 1)] + [CommandLineArgument(IsPositional = true)] [Description("The operation's index.")] - public int OperationIndex { get; set; } + public int OperationIndex { get; set; } = 1; [CommandLineArgument(ShortName = 'D')] [Description("Provides a date to the application.")] @@ -57,23 +54,4 @@ class ProgramArguments [Description("This is an example of a multi-value argument, which can be repeated multiple times to set more than one value.")] [MultiValueSeparator] public string[]? Values { get; set; } - - public static ProgramArguments? Parse() - { - // Not all options can be set with the ParseOptionsAttribute. - var options = new ParseOptions() - { - // Set the value description of all int arguments to "number", instead of doing it - // separately on each argument. - DefaultValueDescriptions = new Dictionary() - { - { typeof(int), "number" } - }, - // Use our own string provider and usage writer for the custom usage strings. - StringProvider = new CustomStringProvider(), - UsageWriter = new CustomUsageWriter(), - }; - - return CommandLineParser.Parse(options); - } } diff --git a/src/Samples/CustomUsage/README.md b/src/Samples/CustomUsage/README.md index ef6d19f8..c4d4bf97 100644 --- a/src/Samples/CustomUsage/README.md +++ b/src/Samples/CustomUsage/README.md @@ -1,7 +1,7 @@ # Custom usage sample This sample shows the flexibility of Ookii.CommandLine's usage help generation. It uses a custom -[`UsageWriter`][], along with a custom `LocalizedStringProvider`, to completely transform the way +[`UsageWriter`][], along with a custom [`LocalizedStringProvider`][], to completely transform the way the usage help looks. This sample also uses long/short parsing mode, but everything in it is applicable to default mode as @@ -50,4 +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. -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`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 diff --git a/src/Samples/LongShort/LongShort.csproj b/src/Samples/LongShort/LongShort.csproj index b34cddbb..d8aec725 100644 --- a/src/Samples/LongShort/LongShort.csproj +++ b/src/Samples/LongShort/LongShort.csproj @@ -1,8 +1,8 @@ - + Exe - net6.0 + net7.0 enable enable Command line parsing sample for Ookii.CommandLine, using long/short parsing mode. @@ -10,8 +10,15 @@ This is sample code, so you can use it freely. + + diff --git a/src/Samples/LongShort/Program.cs b/src/Samples/LongShort/Program.cs index f0af59d6..d811634a 100644 --- a/src/Samples/LongShort/Program.cs +++ b/src/Samples/LongShort/Program.cs @@ -1,39 +1,41 @@ -using Ookii.CommandLine; +using LongShort; +using Ookii.CommandLine; -namespace LongShort; - -internal static class Program +// Not all options can be set with the ParseOptionsAttribute. +var options = new ParseOptions() { - // No need to use the Main(string[] args) overload (though you can if you want), because - // CommandLineParser can take the arguments directly from Environment.GetCommandLineArgs(). - public static int Main() + // Set the value description of all int arguments to "number", instead of doing it + // separately on each argument. + DefaultValueDescriptions = new Dictionary() { - // Parse the arguments. See ProgramArguments.cs for the definitions. - var arguments = ProgramArguments.Parse(); + { typeof(int), "number" } + }, +}; - // No need to do anything when the value is null; Parse() already printed errors and - // usage to the console. We return a non-zero value to indicate failure. - if (arguments == null) - { - return 1; - } +// Use the generated Parse() method. +var arguments = ProgramArguments.Parse(options); - // We use the LineWrappingTextWriter to neatly wrap console output. - using var writer = LineWrappingTextWriter.ForConsoleOut(); +// No need to do anything when the value is null; Parse() already printed errors and +// usage to the console. We return a non-zero value to indicate failure. +if (arguments == null) +{ + return 1; +} - // Print the values of the arguments. - writer.WriteLine("The following argument values were provided:"); - writer.WriteLine($"Source: {arguments.Source}"); - writer.WriteLine($"Destination: {arguments.Destination}"); - writer.WriteLine($"OperationIndex: {arguments.OperationIndex}"); - writer.WriteLine($"Date: {arguments.Date?.ToString() ?? "(null)"}"); - writer.WriteLine($"Count: {arguments.Count}"); - writer.WriteLine($"Verbose: {arguments.Verbose}"); - writer.WriteLine($"Process: {arguments.Process}"); - var values = arguments.Values == null ? "(null)" : "{ " + string.Join(", ", arguments.Values) + " }"; - writer.WriteLine($"Values: {values}"); - writer.WriteLine($"Day: {arguments.Day?.ToString() ?? "(null)"}"); +// We use the LineWrappingTextWriter to neatly wrap console output. +using var writer = LineWrappingTextWriter.ForConsoleOut(); - return 0; - } -} +// Print the values of the arguments. +writer.WriteLine("The following argument values were provided:"); +writer.WriteLine($"Source: {arguments.Source}"); +writer.WriteLine($"Destination: {arguments.Destination}"); +writer.WriteLine($"OperationIndex: {arguments.OperationIndex}"); +writer.WriteLine($"Date: {arguments.Date?.ToString() ?? "(null)"}"); +writer.WriteLine($"Count: {arguments.Count}"); +writer.WriteLine($"Verbose: {arguments.Verbose}"); +writer.WriteLine($"Process: {arguments.Process}"); +var values = arguments.Values == null ? "(null)" : "{ " + string.Join(", ", arguments.Values) + " }"; +writer.WriteLine($"Values: {values}"); +writer.WriteLine($"Day: {arguments.Day?.ToString() ?? "(null)"}"); + +return 0; diff --git a/src/Samples/LongShort/ProgramArguments.cs b/src/Samples/LongShort/ProgramArguments.cs index b3ba63aa..58c64c1e 100644 --- a/src/Samples/LongShort/ProgramArguments.cs +++ b/src/Samples/LongShort/ProgramArguments.cs @@ -8,38 +8,36 @@ namespace LongShort; // sample, so see that sample for more detailed descriptions. // // We use the ParseOptionsAttribute attribute to customize the behavior here, instead of passing -// ParseOptions to the CommandLineParser.Parse() method. +// ParseOptions to the Parse() method. // // This sample uses the alternate long/short parsing mode, transforms argument and value description -// names to dash-case, and uses case sensitive argument name matching. This makes the behavior -// similar to POSIX conventions for command line arguments. +// names to dash-case, and uses case sensitive argument name matching. The IsPosix property sets all +// these behaviors for convenience. This makes the behavior similar to POSIX conventions for command +// line arguments. // // The name transformation is applied to all automatically derived names, but also to the names // of the automatic help and version argument, which are now called "--help" and "--version". +[GeneratedParser] [ApplicationFriendlyName("Ookii.CommandLine Long/Short Mode Sample")] [Description("Sample command line application using long/short parsing mode. The application parses the command line and prints the results, but otherwise does nothing and none of the arguments are actually used for anything.")] -[ParseOptions(Mode = ParsingMode.LongShort, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, - CaseSensitive = true, - DuplicateArguments = ErrorMode.Warning)] -class ProgramArguments +[ParseOptions(IsPosix = true, DuplicateArguments = ErrorMode.Warning)] +partial class ProgramArguments { // This argument has a short name, derived from the first letter of its long name. The long // name is "--source", and the short name is "-s". - [CommandLineArgument(Position = 0, IsRequired = true, IsShort = true)] + [CommandLineArgument(IsPositional = true, IsShort = true)] [Description("The source data.")] - public string? Source { get; set; } + public required string Source { get; set; } // Similarly, this argument has a long name "--destination", and a short name "-d". - [CommandLineArgument(Position = 1, IsRequired = true, IsShort = true)] + [CommandLineArgument(IsPositional = true, IsShort = true)] [Description("The destination data.")] - public string? Destination { get; set; } + public required string Destination { get; set; } // This argument does not have a short name. Its long name is "--operation-index". - [CommandLineArgument(Position = 2, DefaultValue = 1)] + [CommandLineArgument(IsPositional = true)] [Description("The operation's index.")] - public int OperationIndex { get; set; } + public int OperationIndex { get; set; } = 1; // This argument has the long name "--date" and the short name "-D", explicitly specified to // make it uppercase, and distinguish it from the lower case "-d" for "--destination". @@ -63,12 +61,13 @@ class ProgramArguments // // Instead of the alias used in the Parser samples, this argument now has a short name. Note // that you can still use aliases in LongShort mode. Long name aliases are given with the - // AliasAttribute, and short name aliases with the ShortAliasAttribute. + // AliasAttribute, and short name aliases with the ShortAliasAttribute. Automatic prefix + // aliases work for the long names of arguments. [CommandLineArgument(IsShort = true)] [Description("Print verbose information; this is an example of a switch argument.")] public bool Verbose { get; set; } - // Another switch argument, called "--process" with the short name "-v". Switch arguments with + // Another switch argument, called "--process" with the short name "-p". Switch arguments with // short names can be combined; for example, "-vp" sets both the verbose and process switch // (this only works for switch arguments). [CommandLineArgument(IsShort = true)] @@ -88,24 +87,4 @@ class ProgramArguments [Description("This is an example of a multi-value argument, which can be repeated multiple times to set more than one value.")] [MultiValueSeparator] public string[]? Values { get; set; } - - public static ProgramArguments? Parse() - { - // Not all options can be set with the ParseOptionsAttribute. - var options = new ParseOptions() - { - // Set the value description of all int arguments to "number", instead of doing it - // separately on each argument. - DefaultValueDescriptions = new Dictionary() - { - { typeof(int), "number" } - }, - // If you have a lot of arguments, showing full help if there's a parsing error - // can make the error message hard to spot. We set it to show syntax only here, - // and require the use of the "--help" argument for full help. - ShowUsageOnError = UsageHelpRequest.SyntaxOnly, - }; - - return CommandLineParser.Parse(options); - } } diff --git a/src/Samples/LongShort/README.md b/src/Samples/LongShort/README.md index 5a6b3257..6b337975 100644 --- a/src/Samples/LongShort/README.md +++ b/src/Samples/LongShort/README.md @@ -1,11 +1,21 @@ -# Long/short mode sample +# Long/short mode sample This sample alters the behavior of Ookii.CommandLine to be more like the POSIX conventions for command line arguments. To do this, it enables the alternate long/short parsing mode, uses a [name transformation](../../../docs/DefiningArguments.md#name-transformation) to make all the argument names lower case with dashes between the words, and uses case-sensitive argument names. -This sample uses the same arguments as the [parser Sample](../Parser), so see that sample's source +The [`ParseOptionsAttribute.IsPosix`][] property is used to enable all these options at once. It is +equivalent to the following: + +```csharp +[ParseOptions(Mode = ParsingMode.LongShort, + CaseSensitive = true, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase)] +``` + +This sample uses the same arguments as the [parser sample](../Parser), so see that sample's source for more details about each argument. In long/short mode, each argument can have a long name, using the `--` prefix, and a one-character @@ -63,4 +73,6 @@ Note that there is both a `-d` and a `-D` argument, possible due to the use of c argument names. Long/short mode allows you to combine switches with short names, so running `LongShort -vp` sets -both `Verbose` and `Process` to true. +both `--verbose` and `--process` to true. + +[`ParseOptionsAttribute.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm diff --git a/src/Samples/NestedCommands/BaseCommand.cs b/src/Samples/NestedCommands/BaseCommand.cs index 53fa2a10..ca529ec5 100644 --- a/src/Samples/NestedCommands/BaseCommand.cs +++ b/src/Samples/NestedCommands/BaseCommand.cs @@ -11,9 +11,9 @@ namespace NestedCommands; internal abstract class BaseCommand : AsyncCommandBase { // The path argument can be used by any command that inherits from this class. - [CommandLineArgument(DefaultValue = "data.json")] + [CommandLineArgument] [Description("The json file holding the data.")] - public string Path { get; set; } = string.Empty; + public string Path { get; set; } = "data.json"; // Implement the task's RunAsync method to load the database and handle some errors. public override async Task RunAsync() diff --git a/src/Samples/NestedCommands/CourseCommands.cs b/src/Samples/NestedCommands/CourseCommands.cs index 354d41c5..8ea18ff1 100644 --- a/src/Samples/NestedCommands/CourseCommands.cs +++ b/src/Samples/NestedCommands/CourseCommands.cs @@ -15,20 +15,21 @@ internal class CourseCommand : ParentCommand // Command to add courses. Since it inherits from BaseCommand, it has a Path argument in addition // to the arguments created here. +[GeneratedParser] [Command("add")] [ParentCommand(typeof(CourseCommand))] [Description("Adds a course to the database.")] -internal class AddCourseCommand : BaseCommand +internal partial class AddCourseCommand : BaseCommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The name of the course.")] [ValidateNotWhiteSpace] - public string Name { get; set; } = string.Empty; + public required string Name { get; set; } - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The name of the teacher of the course.")] [ValidateNotWhiteSpace] - public string Teacher { get; set; } = string.Empty; + public required string Teacher { get; set; } protected override async Task RunAsync(Database db) { @@ -42,15 +43,16 @@ protected override async Task RunAsync(Database db) // Command to remove courses. Since it inherits from BaseCommand, it has a Path argument in addition // to the arguments created here. +[GeneratedParser] [Command("remove")] [ParentCommand(typeof(CourseCommand))] [Description("Removes a course from the database.")] -internal class RemoveCourseCommand : BaseCommand +internal partial class RemoveCourseCommand : BaseCommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The ID of the course to remove.")] - public int Id { get; set; } + public required int Id { get; set; } protected override async Task RunAsync(Database db) { diff --git a/src/Samples/NestedCommands/CustomUsageWriter.cs b/src/Samples/NestedCommands/CustomUsageWriter.cs deleted file mode 100644 index faf69937..00000000 --- a/src/Samples/NestedCommands/CustomUsageWriter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Ookii.CommandLine; -using Ookii.CommandLine.Commands; -using System.Diagnostics.CodeAnalysis; - -namespace NestedCommands; - -// UsageWriter used by parent commands to show a list of child commands. -internal class CustomUsageWriter : UsageWriter -{ - private readonly CommandInfo _command; - - public CustomUsageWriter(CommandInfo command) - { - _command = command; - } - - // Override this to add the parent command name. - [AllowNull] - public override string ExecutableName - { - get => base.ExecutableName + " " + _command.Name; - set => base.ExecutableName = value; - } - - // Override this to return the command description instead of the application description. - protected override void WriteApplicationDescription(string description) - { - // Don't modify anything if this is usage help for a child command's arguments. - if (OperationInProgress != Operation.CommandListUsage) - { - base.WriteApplicationDescription(description); - return; - } - - Writer.Indent = ShouldIndent ? ApplicationDescriptionIndent : 0; - Writer.WriteLine(_command.Description); - Writer.WriteLine(); - } - - // Override this to make it clear these are nested commands. - protected override void WriteAvailableCommandsHeader() - { - Writer.WriteLine($"The '{_command.Name}' command has the following subcommands:"); - Writer.WriteLine(); - } -} diff --git a/src/Samples/NestedCommands/GeneratedManager.cs b/src/Samples/NestedCommands/GeneratedManager.cs new file mode 100644 index 00000000..c1a32526 --- /dev/null +++ b/src/Samples/NestedCommands/GeneratedManager.cs @@ -0,0 +1,9 @@ +using Ookii.CommandLine.Commands; + +namespace NestedCommands; + +// Use source generation to locate commands in this assembly. +[GeneratedCommandManager] +internal partial class GeneratedManager +{ +} diff --git a/src/Samples/NestedCommands/ListCommand.cs b/src/Samples/NestedCommands/ListCommand.cs index a14e7835..6052a362 100644 --- a/src/Samples/NestedCommands/ListCommand.cs +++ b/src/Samples/NestedCommands/ListCommand.cs @@ -6,9 +6,10 @@ namespace NestedCommands; // A top-level command that lists all the values in the database. Since it inherits from // BaseCommand, it has a Path argument even though no arguments are defined here +[GeneratedParser] [Command("list")] [Description("Lists all students and courses.")] -internal class ListCommand : BaseCommand +internal partial class ListCommand : BaseCommand { protected override Task RunAsync(Database db) { diff --git a/src/Samples/NestedCommands/NestedCommands.csproj b/src/Samples/NestedCommands/NestedCommands.csproj index 933f5bd1..7e51c7b7 100644 --- a/src/Samples/NestedCommands/NestedCommands.csproj +++ b/src/Samples/NestedCommands/NestedCommands.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 enable enable Nested subcommands sample for Ookii.CommandLine. @@ -10,8 +10,13 @@ This is sample code, so you can use it freely. + + diff --git a/src/Samples/NestedCommands/ParentCommand.cs b/src/Samples/NestedCommands/ParentCommand.cs deleted file mode 100644 index 9de64d00..00000000 --- a/src/Samples/NestedCommands/ParentCommand.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Ookii.CommandLine.Commands; -using System.Reflection; - -namespace NestedCommands; - -// This is the base class for all the commands that have child commands. It performs all -// the work necessary to find and run subcommands, so derived classes don't have to do anything -// except add the [Command] attribute. -// -// This class uses ICommandWithCustomParsing, so CommandLineParser won't be used to create it. -// Instead, CommandManager will just instantiate it and call the Parse method, where we can -// do whatever we want. In this case, we create another CommandManager to find and create a -// child command. -// -// Although this sample doesn't do this, you can use this to nest commands more than one -// level deep just as easily. -internal abstract class ParentCommand : AsyncCommandBase, ICommandWithCustomParsing -{ - private IAsyncCommand? _childCommand; - - public void Parse(string[] args, int index, CommandOptions options) - { - // Nested commands don't need to have a "version" command. - options.AutoVersionCommand = false; - - // Select only the commands that have a ParentCommandAttribute specifying this command - // as their parent. - options.CommandFilter = - (command) => command.CommandType.GetCustomAttribute()?.ParentCommand == GetType(); - - var manager = new CommandManager(options); - var info = new CommandInfo(GetType(), manager); - - // Use a custom UsageWriter to replace the application description with the - // description of this command. - options.UsageWriter = new CustomUsageWriter(info) - { - // Apply the same options as the parent command. - IncludeApplicationDescriptionBeforeCommandList = true, - IncludeCommandHelpInstruction = true, - }; - - // All commands in this sample are async, so this cast is safe. - _childCommand = (IAsyncCommand?)manager.CreateCommand(args, index); - } - - public override async Task RunAsync() - { - // If the child command had a parsing error, it won't have been created. - if (_childCommand == null) - { - return (int)ExitCode.CreateCommandFailure; - } - - // Otherwise, we can run the command. - return await _childCommand.RunAsync(); - } -} diff --git a/src/Samples/NestedCommands/ParentCommandAttribute.cs b/src/Samples/NestedCommands/ParentCommandAttribute.cs deleted file mode 100644 index e54c9123..00000000 --- a/src/Samples/NestedCommands/ParentCommandAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace NestedCommands; - -// We use this attribute to distinguish top-level commands from child commands. -// -// Top-level commands won't have this attribute. Child commands will have it with the type of -// their parent command set. -[AttributeUsage(AttributeTargets.Class)] -internal class ParentCommandAttribute : Attribute -{ - private readonly Type _parentCommand; - - public ParentCommandAttribute(Type parentCommand) - { - ArgumentNullException.ThrowIfNull(parentCommand); - _parentCommand = parentCommand; - } - - public Type ParentCommand => _parentCommand; -} diff --git a/src/Samples/NestedCommands/Program.cs b/src/Samples/NestedCommands/Program.cs index ea7567a3..8e307e50 100644 --- a/src/Samples/NestedCommands/Program.cs +++ b/src/Samples/NestedCommands/Program.cs @@ -1,27 +1,20 @@ -using Ookii.CommandLine; +using NestedCommands; +using Ookii.CommandLine; using Ookii.CommandLine.Commands; -namespace NestedCommands; - -internal static class Program +var options = new CommandOptions() { - static async Task Main() + UsageWriter = new UsageWriter() { - var options = new CommandOptions() - { - // For the top-level, we only want commands that don't have the ParentCommandAttribute. - CommandFilter = (command) => !Attribute.IsDefined(command.CommandType, typeof(ParentCommandAttribute)), - UsageWriter = new UsageWriter() - { - // Add the application description. - IncludeApplicationDescriptionBeforeCommandList = true, - // Commands with child commands don't technically have a -Help argument, but they'll - // ignore it and print their child command list anyway, so let's show the message. - IncludeCommandHelpInstruction = true, - }, - }; + // Add the application description. + IncludeApplicationDescriptionBeforeCommandList = true, + // The commands that derive from ParentCommand use ICommandWithCustomParsing, and don't + // technically have a -Help argument. This prevents the instruction from being shown by + // default. However, these commands will ignore -Help ignore it and print their child + // command list anyway, so force the message to be shown. + IncludeCommandHelpInstruction = true, + }, +}; - var manager = new CommandManager(options); - return await manager.RunCommandAsync() ?? (int)ExitCode.CreateCommandFailure; - } -} +var manager = new GeneratedManager(options); +return await manager.RunCommandAsync() ?? (int)ExitCode.CreateCommandFailure; diff --git a/src/Samples/NestedCommands/README.md b/src/Samples/NestedCommands/README.md index 561b6012..877d138b 100644 --- a/src/Samples/NestedCommands/README.md +++ b/src/Samples/NestedCommands/README.md @@ -1,28 +1,25 @@ # Nested commands sample -While Ookii.CommandLine has no built-in way to nest subcommands, such functionality is easy to -implement using the [`CommandOptions.CommandFilter`][] property. All you need is a way to -distinguish top-level commands and child commands. +This sample demonstrates how to use the [`ParentCommandAttribute`][] attribute and the [`ParentCommand`][] +class to build an application that has commands with nested subcommands. Commands with the +[`ParentCommandAttribute`][] are nested under the specified command, and commands without this attribute +are top-level commands. -This sample demonstrates one way to do this. It defines a [`ParentCommandAttribute`](ParentCommandAttribute.cs) -that can be used to specify which command is the parent of a command, and commands without this -attribute are top-level commands. - -Commands that have children use the [`ICommandWithCustomParsing`][] interface so they can do their -own parsing, rather than relying on the [`CommandLineParser`][]. This allows them to create a new -[`CommandManager`][] that filters only the children of that command, and passes the remaining -arguments to that. Check the [ParentCommand.cs](ParentCommand.cs) file to see how this works. +Commands that have children derive from the [`ParentCommand`][] class. This class will use your +[`CommandManager`][], but sets the [`CommandOptions.ParentCommand`][] property to filter only the +children of that command. The remaining arguments are passed to the nested subcommand. Child commands are just regular commands using the [`CommandLineParser`][], and don't need to do -anything special except to add the `ParentCommandAttribute` attribute to specify which command is +anything special except to add the [`ParentCommandAttribute`][] attribute to specify which command is their parent. For an example, see [CourseCommands.cs](CourseCommands.cs). -This sample uses this framework to create a simple "database" application that lets your add and -remove students and courses to a json file. It has top-level commands `student` and `course`, which -both have child commands `add` and `remove` (and a few others). +This sample creates a simple "database" application that lets you add and remove students and +courses to a json file. It has top-level commands `student` and `course`, which both have child +commands `add` and `remove` (and a few others). All the leaf commands use a common base class, so they can specify the path to the json file. This -is the way you add common arguments to multiple commands in Ookii.CommandLine. +is the primary way you add common arguments to multiple commands in Ookii.CommandLine (for an +alternative, see the [top-level arguments sample](../TopLevelArguments)). When invoked without arguments, we see only the top-level commands: @@ -57,7 +54,7 @@ Add or remove a student. Usage: NestedCommands student [arguments] -The 'student' command has the following subcommands: +The following commands are available: add Adds a student to the database. @@ -71,19 +68,16 @@ The 'student' command has the following subcommands: Run 'NestedCommands student -Help' for more information about a command. ``` -You can see the sample has customized the parent command to: +You can see the parent command will: - Show the command description at the top, rather than the application description. - Include the top-level command name in the usage syntax. -- Change the header above the commands to indicate these are nested subcommands. -- Remove the a `version` command (nested version commands would kind of redundant). - -This was done by changing the [`CommandOptions`][] and using a simple custom -[`UsageWriter`][] derived class (see [CustomUsageWriter.cs](CustomUsageWriter.cs)). +- Show only its child commands (which also excludes the `version` command). If we run `./NestedCommand student -Help`, we get the same output. While the `student` command -doesn't have a help argument (since it uses custom parsing, and not the [`CommandLineParser`][]), -there is no command named `-Help` so it still just shows the command list. +doesn't have a help argument (since the [`ParentCommand`][] uses [`ICommandWithCustomParsing`][], +and not the [`CommandLineParser`][]), there is no command named `-Help` so it still just shows the +command list. If we run `./NestedCommand student add -Help`, we get the help for the command's arguments as usual: @@ -110,11 +104,11 @@ Usage: NestedCommands student add [-FirstName] [-LastName] [[- The json file holding the data. Default value: data.json. ``` -We can see the usage syntax correctly shows both command names before the arguments. +The usage syntax shows both command names before the arguments. -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.htm -[`CommandManager`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandManager.htm -[`CommandOptions.CommandFilter`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter.htm -[`CommandOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandOptions.htm -[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.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 +[`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 diff --git a/src/Samples/NestedCommands/StudentCommands.cs b/src/Samples/NestedCommands/StudentCommands.cs index 760d2ce0..b407dc35 100644 --- a/src/Samples/NestedCommands/StudentCommands.cs +++ b/src/Samples/NestedCommands/StudentCommands.cs @@ -15,22 +15,23 @@ internal class StudentCommand : ParentCommand // Command to add students. Since it inherits from BaseCommand, it has a Path argument in addition // to the arguments created here. +[GeneratedParser] [Command("add")] [ParentCommand(typeof(StudentCommand))] [Description("Adds a student to the database.")] -internal class AddStudentCommand : BaseCommand +internal partial class AddStudentCommand : BaseCommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The first name of the student.")] [ValidateNotWhiteSpace] - public string FirstName { get; set; } = string.Empty; + public required string FirstName { get; set; } - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The last name of the student.")] [ValidateNotWhiteSpace] - public string LastName { get; set; } = string.Empty; + public required string LastName { get; set; } - [CommandLineArgument(Position = 2)] + [CommandLineArgument(IsPositional = true)] [Description("The student's major.")] public string? Major { get; set; } @@ -46,15 +47,16 @@ protected override async Task RunAsync(Database db) // Command to remove students. Since it inherits from BaseCommand, it has a Path argument in // addition to the arguments created here. +[GeneratedParser] [Command("remove")] [ParentCommand(typeof(StudentCommand))] [Description("Removes a student from the database.")] -internal class RemoveStudentCommand : BaseCommand +internal partial class RemoveStudentCommand : BaseCommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The ID of the student to remove.")] - public int Id { get; set; } + public required int Id { get; set; } protected override async Task RunAsync(Database db) { @@ -72,23 +74,24 @@ protected override async Task RunAsync(Database db) // Command to add a course to a student. Since it inherits from BaseCommand, it has a Path argument // in addition to the arguments created here. +[GeneratedParser] [Command("add-course")] [ParentCommand(typeof(StudentCommand))] [Description("Adds a course for a student.")] -internal class AddStudentCourseCommand : BaseCommand +internal partial class AddStudentCourseCommand : BaseCommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The ID of the student.")] - public int StudentId { get; set; } + public required int StudentId { get; set; } - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The ID of the course.")] - public int CourseId { get; set; } + public required int CourseId { get; set; } - [CommandLineArgument(Position = 2, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The grade achieved in the course.")] [ValidateRange(1.0f, 10.0f)] - public float Grade { get; set; } + public required float Grade { get; set; } protected override async Task RunAsync(Database db) { diff --git a/src/Samples/Parser/Parser.csproj b/src/Samples/Parser/Parser.csproj index aae67597..0eea551f 100644 --- a/src/Samples/Parser/Parser.csproj +++ b/src/Samples/Parser/Parser.csproj @@ -2,16 +2,23 @@ Exe - net6.0 - disable + net7.0 + enable enable Command line parsing sample for Ookii.CommandLine Copyright (c) Sven Groot (Ookii.org) This is sample code, so you can use it freely. + + diff --git a/src/Samples/Parser/Program.cs b/src/Samples/Parser/Program.cs index 70bb8031..ac89aa76 100644 --- a/src/Samples/Parser/Program.cs +++ b/src/Samples/Parser/Program.cs @@ -1,38 +1,48 @@ using Ookii.CommandLine; +using ParserSample; -namespace ParserSample; - -public static class Program +// Many aspects of the parsing behavior and usage help generation can be customized using the +// ParseOptions. You can also use the ParseOptionsAttribute for some of them (see the LongShort +// sample for an example of that). +var options = new ParseOptions() { - // No need to use the Main(string[] args) overload (though you can if you want), because - // CommandLineParser can take the arguments directly from Environment.GetCommandLineArgs(). - public static int Main() - { - // Parse the arguments. See ProgramArguments.cs for the definitions. - var arguments = ProgramArguments.Parse(); + // By default, repeating an argument more than once (except for multi-value arguments), causes + // an error. By changing this option, we set it to show a warning instead, and use the last + // value supplied. + DuplicateArguments = ErrorMode.Warning, +}; - // No need to do anything when the value is null; Parse() already printed errors and - // usage help to the console. We return a non-zero value to indicate failure. - if (arguments == null) - { - return 1; - } +// The GeneratedParserAttribute adds a static Parse method to your class, which parses the +// arguments, handles errors, and shows usage help if necessary (using a LineWrappingTextWriter to +// neatly white-space wrap console output). +// +// It takes the arguments from Environment.GetCommandLineArgs(), but also has an overload +// that takes a string[] array, if you prefer. +// +// If you want more control over parsing and error handling, you can create an instance of +// the CommandLineParser class. See docs/ParsingArguments.md for an example of that. +var arguments = ProgramArguments.Parse(options); - // We use the LineWrappingTextWriter to neatly white-space wrap console output. - using var writer = LineWrappingTextWriter.ForConsoleOut(); +// No need to do anything when the value is null; Parse() already printed errors and +// usage help to the console. We return a non-zero value to indicate failure. +if (arguments == null) +{ + return 1; +} - // Print the values of the arguments. - writer.WriteLine("The following argument values were provided:"); - writer.WriteLine($"Source: {arguments.Source}"); - writer.WriteLine($"Destination: {arguments.Destination}"); - writer.WriteLine($"OperationIndex: {arguments.OperationIndex}"); - writer.WriteLine($"Date: {arguments.Date?.ToString() ?? "(null)"}"); - writer.WriteLine($"Count: {arguments.Count}"); - writer.WriteLine($"Verbose: {arguments.Verbose}"); - var values = arguments.Values == null ? "(null)" : "{ " + string.Join(", ", arguments.Values) + " }"; - writer.WriteLine($"Values: {values}"); - writer.WriteLine($"Day: {arguments.Day?.ToString() ?? "(null)"}"); +// We use the LineWrappingTextWriter to neatly white-space wrap console output. +using var writer = LineWrappingTextWriter.ForConsoleOut(); - return 0; - } -} +// Print the values of the arguments. +writer.WriteLine("The following argument values were provided:"); +writer.WriteLine($"Source: {arguments.Source}"); +writer.WriteLine($"Destination: {arguments.Destination}"); +writer.WriteLine($"OperationIndex: {arguments.OperationIndex}"); +writer.WriteLine($"Date: {arguments.Date?.ToString() ?? "(null)"}"); +writer.WriteLine($"Count: {arguments.Count}"); +writer.WriteLine($"Verbose: {arguments.Verbose}"); +var values = arguments.Values == null ? "(null)" : "{ " + string.Join(", ", arguments.Values) + " }"; +writer.WriteLine($"Values: {values}"); +writer.WriteLine($"Day: {arguments.Day?.ToString() ?? "(null)"}"); + +return 0; diff --git a/src/Samples/Parser/ProgramArguments.cs b/src/Samples/Parser/ProgramArguments.cs index 9e97ed2f..8b1d33ae 100644 --- a/src/Samples/Parser/ProgramArguments.cs +++ b/src/Samples/Parser/ProgramArguments.cs @@ -1,6 +1,5 @@ using Ookii.CommandLine; using Ookii.CommandLine.Validation; -using System; using System.ComponentModel; namespace ParserSample; @@ -13,9 +12,15 @@ namespace ParserSample; // // We add a friendly name for the application, used by the "-Version" argument, and a description // used when displaying usage help. +// +// The GeneratedParserAttribute indicates this class uses source generation, building the parser at +// compile time instead of during runtime. This gives us improved performance, some additional +// features, and compile-time errors and warnings. Arguments classes that use the +// GeneratedParserAttribute must be partial. +[GeneratedParser] [ApplicationFriendlyName("Ookii.CommandLine Sample")] [Description("Sample command line application. The application parses the command line and prints the results, but otherwise does nothing and none of the arguments are actually used for anything.")] -class ProgramArguments +partial class ProgramArguments { // This property defines a required positional argument called "-Source". // @@ -31,27 +36,34 @@ class ProgramArguments // fails. // // We add a description that will be shown when displaying usage help. - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The source data.")] - public string? Source { get; set; } + public required string Source { get; set; } + + // If not using .Net 7 and C# 11 or later, the required keyword is not available. In that case, + // use the following to create a required argument: + // [CommandLineArgument(IsRequired = true, IsPositional = true)] + // [Description("The source data.")] + // public string? Source { get; set; } + // This property defines a required positional argument called "-Destination". - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The destination data.")] - public string? Destination { get; set; } + public required string Destination { get; set; } - // This property defines a optional positional argument called "-OperationIndex". If the argument - // is not supplied, this property will be set to the default value 1. + // This property defines a optional positional argument called "-OperationIndex". If the + // argument is not supplied, this property will be set to the default value 1. This default + // value will also be shown in the usage help. // // The argument's type is "int", so only valid integer values will be accepted. Anything else // will cause an error. // - // For types other than string, CommandLineParser will use the TypeConverter for the argument's - // type to try to convert the string to the correct type. It can also convert types with a - // public static Parse method, or with a constructor that takes a string. - [CommandLineArgument(Position = 2, DefaultValue = 1)] + // For types other than string, Ookii.CommandLine can use any type with a public static Parse + // method (preferably ISpanParsable in .Net 7), or with a constructor that takes a string. + [CommandLineArgument(IsPositional = true)] [Description("The operation's index.")] - public int OperationIndex { get; set; } + public int OperationIndex { get; set; } = 1; // This property defines an argument named "-Date". This argument is not positional, so it can // only be supplied by name, for example as "-Date 1969-07-20". @@ -60,7 +72,7 @@ class ProgramArguments // supplied, rather than having to choose a default value. Since there is no default value, the // CommandLineParser won't set this property at all if the argument is not supplied. // - // The type conversion from string to DateTime is culture sensitive. The CommandLineParser + // The conversion from string to DateTime is culture sensitive. The CommandLineParser // defaults to CultureInfo.InvariantCulture to ensure a consistent experience regardless of the // user's culture, though you can change that if you want. [CommandLineArgument] @@ -74,7 +86,8 @@ class ProgramArguments // // It uses a validator that ensures the value is within the specified range. The usage help will // show that requirement as well. - [CommandLineArgument(ValueDescription = "Number")] + [CommandLineArgument] + [ValueDescription("Number")] [Description("Provides the count for something to the application.")] [ValidateRange(0, 100)] public int Count { get; set; } @@ -93,6 +106,11 @@ class ProgramArguments // // This argument has an alias, so it can also be specified using "-v" instead of its regular // name. An argument can have multiple aliases by specifying the Alias attribute more than once. + // + // Any unique prefix of an argument name or alias is also an alias, unless + // ParseOptions.AutoPrefixAliases is set to false. The prefix "v", however, is not unique, since + // it could be for either "-Verbose" or "-Version", so it won't work unless specifically added + // as an alias. However, e.g. "-Verb" will work as an alias automatically. [CommandLineArgument] [Description("Print verbose information; this is an example of a switch argument.")] [Alias("v")] @@ -126,35 +144,4 @@ class ProgramArguments [Description("This is an argument using an enumeration type.")] [ValidateEnumValue] public DayOfWeek? Day { get; set; } - - // Using a static creation function for a command line arguments class is not required, but it's - // a convenient way to place all command-line related functionality in one file. To parse the - // arguments (eg. from the Main method) you only need to call this function. - public static ProgramArguments? Parse() - { - // Many aspects of the parsing behavior and usage help generation can be customized using - // the ParseOptions. You can also use the ParseOptionsAttribute for some of them (see the - // LongShort sample for an example of that). - var options = new ParseOptions() - { - // If you have a lot of arguments, showing full help if there's a parsing error can make - // the error message hard to spot. We set it to show syntax only here, and require the - // use of the "-Help" argument for full help. - ShowUsageOnError = UsageHelpRequest.SyntaxOnly, - // By default, repeating an argument more than once (except for multi-value arguments), - // causes an error. By changing this option, we set it to show a warning instead, and - // use the last value supplied. - DuplicateArguments = ErrorMode.Warning, - }; - - // The static Parse method parses the arguments, handles errors, and shows usage help if - // necessary (using a LineWrappingTextWriter to neatly white-space wrap console output). - // - // It takes the arguments from Environment.GetCommandLineArgs(), but also has an overload - // that takes a string[] array, if you prefer. - // - // If you want more control over parsing and error handling, you can create an instance of - // the CommandLineParser class. See docs/ParsingArguments.md for an example of that. - return CommandLineParser.Parse(options); - } } diff --git a/src/Samples/Parser/README.md b/src/Samples/Parser/README.md index 1ceb119f..758efe29 100644 --- a/src/Samples/Parser/README.md +++ b/src/Samples/Parser/README.md @@ -2,21 +2,21 @@ This sample shows the basic functionality of Ookii.CommandLine. It shows you how to define a number of arguments with different types and options, and how to parse them. It then prints the value of -the supplied arguments (it does nothing else). +the supplied arguments (it does nothing else). It also uses [source generation](../../../docs/SourceGeneration.md). The sample contains detailed information about every step it takes, so it should be a good learning -resource to get started. Check [ProgramArguments.cs](ProgramArguments.cs) for the arguments, and -[Program.cs](Program.cs) for the main function. +resource to get started, along with the [tutorial](../../../docs/Tutorial.md). Check +[ProgramArguments.cs](ProgramArguments.cs) for the arguments, and [Program.cs](Program.cs) for the +main function. This sample prints the following usage help, when invoked with the `-Help` argument: ```text -Sample command line application. The application parses the command line and prints the results, -but otherwise does nothing and none of the arguments are actually used for anything. +Sample command line application. The application parses the command line and prints the results, but +otherwise does nothing and none of the arguments are actually used for anything. Usage: Parser [-Source] [-Destination] [[-OperationIndex] ] [-Count - ] [-Date ] [-Day ] [-Help] [-Value ...] [-Verbose] - [-Version] + ] [-Date ] [-Day ] [-Help] [-Value ...] [-Verbose] [-Version] -Source The source data. @@ -74,21 +74,21 @@ Usage: Parser [-Source] [-Destination] [[-OperationIndex] +// An ArgumentConverter for the Encoding class. +internal class EncodingConverter : ArgumentConverter { - protected override Encoding? Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { try { @@ -18,7 +16,7 @@ internal class EncodingConverter : TypeConverterBase } catch (ArgumentException ex) { - // This is the expected exception type for a type converter. + // This is the expected exception type for a converter. throw new FormatException(ex.Message, ex); } } diff --git a/src/Samples/Subcommand/GeneratedManager.cs b/src/Samples/Subcommand/GeneratedManager.cs new file mode 100644 index 00000000..d0f08525 --- /dev/null +++ b/src/Samples/Subcommand/GeneratedManager.cs @@ -0,0 +1,10 @@ +using Ookii.CommandLine.Commands; + +namespace SubcommandSample; + +// Use source generation to locate commands in this assembly. This, along with the +// GeneratedParserAttribute on all the commands, enables trimming. +[GeneratedCommandManager] +partial class GeneratedManager +{ +} diff --git a/src/Samples/Subcommand/Program.cs b/src/Samples/Subcommand/Program.cs index 81d2c92e..09a07ee0 100644 --- a/src/Samples/Subcommand/Program.cs +++ b/src/Samples/Subcommand/Program.cs @@ -1,7 +1,6 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; using Ookii.CommandLine.Terminal; -using System.Threading.Tasks; // 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. @@ -24,19 +23,17 @@ static async Task Main() CommandNameTransform = NameTransform.DashCase, UsageWriter = new UsageWriter() { - // Since all the commands have an automatic "-Help" argument, show the instruction - // how to get help on a command. - IncludeCommandHelpInstruction = true, // Show the application description before the command list. IncludeApplicationDescriptionBeforeCommandList = true, }, }; - // Create a CommandManager for the commands in the current assembly. + // 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 CommandManager(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 diff --git a/src/Samples/Subcommand/README.md b/src/Samples/Subcommand/README.md index 40cd7bfd..a690d3e8 100644 --- a/src/Samples/Subcommand/README.md +++ b/src/Samples/Subcommand/README.md @@ -4,11 +4,17 @@ This sample is a simple demonstration of subcommands. The sample application def `read`, and `write`, which can be used to read or write a file, respectively. The sample shows how to use both synchronous and asynchronous commands, and also contains an example -of a custom [`TypeConverter`][], used for the [`Encoding`][Encoding_1] class. +of a custom [`ArgumentConverter`][], used for the [`Encoding`][Encoding_1] class. For detailed information, check the source of the [`ReadCommand`](ReadCommand.cs) class, the [`WriteCommand`](WriteCommand.cs) class, and the [`Main()`](Program.cs) method to see it works. +This application uses [source generation](../../../docs/SourceGeneration.md) for both the commands, +and for the [`CommandManager`][] to find all commands and arguments at compile time. This enables +the application to be safely trimmed. You can try this out by running `dotnet publish --self-contained` +in the project's folder. This also works for applications without subcommands, even though this is +the only sample that demonstrates this by setting the `PublishTrimmed` property in the project file. + When invoked without arguments, a subcommand application prints the list of commands. ```text @@ -32,7 +38,7 @@ Run 'Subcommand -Help' for more information about a command. ``` Like the usage help format for arguments, the command list format can also be customized using the -`UsageWriter` class. If the console is capable, the command list also uses color. +[`UsageWriter`][] class. If the console is capable, the command list also uses color. If we run `./Subcommand write -Help`, we get the following: @@ -72,11 +78,13 @@ there is an automatic `version` command, which has the same function. We can see `./Subcommand version`: ```text -Ookii.CommandLine Subcommand Sample 3.0.0 +Ookii.CommandLine Subcommand Sample 4.0.0 Copyright (c) Sven Groot (Ookii.org) This is sample code, so you can use it freely. ``` -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.htm -[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter +[`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 [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 5140d889..c7f3e864 100644 --- a/src/Samples/Subcommand/ReadCommand.cs +++ b/src/Samples/Subcommand/ReadCommand.cs @@ -1,10 +1,8 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; -using System; +using Ookii.CommandLine.Conversion; using System.ComponentModel; -using System.IO; using System.Text; -using System.Threading.Tasks; namespace SubcommandSample; @@ -19,29 +17,24 @@ namespace SubcommandSample; // IAsyncCommand ourselves. // // Check the Program.cs file to see how this command is invoked. +[GeneratedParser] [Command] [Description("Reads and displays data from a file using the specified encoding, wrapping the text to fit the console.")] -[ParseOptions(ArgumentNameTransform = NameTransform.PascalCase)] -class ReadCommand : AsyncCommandBase +partial class ReadCommand : AsyncCommandBase { - private readonly FileInfo _path; - - // The constructor is used to define the path property. Since it's a required argument, it's - // good to use a non-nullable reference type, but FileInfo doesn't have a good default to - // initialize a property with. So, we use the constructor. - // - // The NameTransform makes sure the argument matches the naming style of the other arguments. - public ReadCommand([Description("The name of the file to read.")] FileInfo path) - { - _path = path; - } + // A required, positional argument to specify the file name. + [CommandLineArgument(IsPositional = true)] + [Description("The path of the file to read.")] + public required FileInfo Path { get; set; } // An argument to specify the encoding. - // Because Encoding doesn't have a default TypeConverter, we use a custom one provided in + // Because Encoding doesn't have a default ArgumentConverter, we use a custom one provided in // this sample. - [CommandLineArgument] + // Encoding's ToString() implementation just gives the class name, so don't include the default + // value in the usage help; we'll write it ourself instead. + [CommandLineArgument(IncludeDefaultInUsageHelp = false)] [Description("The encoding to use to read the file. The default value is utf-8.")] - [TypeConverter(typeof(EncodingConverter))] + [ArgumentConverter(typeof(EncodingConverter))] public Encoding Encoding { get; set; } = Encoding.UTF8; // Run the command after the arguments have been parsed. @@ -49,22 +42,11 @@ public override async Task RunAsync() { try { - var options = new FileStreamOptions() - { - Access = FileAccess.Read, - Mode = FileMode.Open, - Share = FileShare.ReadWrite | FileShare.Delete, - Options = FileOptions.Asynchronous - }; - - using var reader = new StreamReader(_path.FullName, Encoding, true, options); - // We use a LineWrappingTextWriter to neatly wrap console output using var writer = LineWrappingTextWriter.ForConsoleOut(); // Write the contents of the file to the console. - string? line; - while ((line = await reader.ReadLineAsync()) != null) + await foreach (var line in File.ReadLinesAsync(Path.FullName, Encoding)) { await writer.WriteLineAsync(line); } diff --git a/src/Samples/Subcommand/Subcommand.csproj b/src/Samples/Subcommand/Subcommand.csproj index abf99ff3..0f4c9a62 100644 --- a/src/Samples/Subcommand/Subcommand.csproj +++ b/src/Samples/Subcommand/Subcommand.csproj @@ -2,16 +2,24 @@ Exe - net6.0 - disable + net7.0 + enable enable Subcommand sample for Ookii.CommandLine. Copyright (c) Sven Groot (Ookii.org) This is sample code, so you can use it freely. + true + + diff --git a/src/Samples/Subcommand/WriteCommand.cs b/src/Samples/Subcommand/WriteCommand.cs index 37332e58..714433b0 100644 --- a/src/Samples/Subcommand/WriteCommand.cs +++ b/src/Samples/Subcommand/WriteCommand.cs @@ -1,16 +1,13 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Validation; -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Text; -using System.Threading.Tasks; namespace SubcommandSample; -// This is a sample subcommand that can be invoked by specifying "read" as the first argument +// This is a sample subcommand that can be invoked by specifying "write" as the first argument // to the sample application. // // Subcommand argument parsing works just like a regular command line argument class. After the @@ -21,43 +18,37 @@ namespace SubcommandSample; // IAsyncCommand ourselves. // // Check the Program.cs file to see how this command is invoked. +[GeneratedParser] [Command] [Description("Writes lines to a file, wrapping them to the specified width.")] -[ParseOptions(ArgumentNameTransform = NameTransform.PascalCase)] -class WriteCommand : AsyncCommandBase +partial class WriteCommand : AsyncCommandBase { - private readonly FileInfo _path; - - // The constructor is used to define the path argument. Since it's a required argument, it's - // good to use a non-nullable reference type, but FileInfo doesn't have a good default to - // initialize a property with. So, we use the constructor. - // - // The NameTransform makes sure the argument matches the naming style of the other arguments. - public WriteCommand([Description("The name of the file to write to.")] FileInfo path) - { - _path = path; - } - + // A required, positional argument to specify the file name. + [CommandLineArgument(IsPositional = true)] + [Description("The path of the file to write to.")] + public required FileInfo Path { get; set; } // Positional multi-value argument to specify the text to write - [CommandLineArgument(Position = 1)] + [CommandLineArgument(IsPositional = true)] [Description("The lines of text to write to the file; if no lines are specified, this application will read from standard input instead.")] public string[]? Lines { get; set; } // An argument to specify the encoding. - // Because Encoding doesn't have a default TypeConverter, we use a custom one provided in + // Because Encoding doesn't have a default ArgumentConverter, we use a custom one provided in // this sample. - [CommandLineArgument] + // Encoding's ToString() implementation just gives the class name, so don't include the default + // value in the usage help; we'll write it ourself instead. + [CommandLineArgument(IncludeDefaultInUsageHelp = false)] [Description("The encoding to use to write the file. Default value: utf-8.")] - [TypeConverter(typeof(EncodingConverter))] + [ArgumentConverter(typeof(EncodingConverter))] public Encoding Encoding { get; set; } = Encoding.UTF8; // An argument that specifies the maximum line length of the output. - [CommandLineArgument(DefaultValue = 79)] + [CommandLineArgument] [Description("The maximum length of the lines in the file, or 0 to have no limit.")] [Alias("Length")] [ValidateRange(0, null)] - public int MaximumLineLength { get; set; } + public int MaximumLineLength { get; set; } = 79; // A switch argument that indicates it's okay to overwrite files. [CommandLineArgument] @@ -70,7 +61,7 @@ public override async Task RunAsync() try { // Check if we're allowed to overwrite the file. - if (!Overwrite && _path.Exists) + if (!Overwrite && Path.Exists) { // 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, @@ -87,7 +78,7 @@ public override async Task RunAsync() Options = FileOptions.Asynchronous }; - using var writer = new StreamWriter(_path.FullName, Encoding, options); + using var writer = new StreamWriter(Path.FullName, Encoding, options); // We use a LineWrappingTextWriter to neatly white-space wrap the output. using var lineWriter = new LineWrappingTextWriter(writer, MaximumLineLength); diff --git a/src/Samples/TopLevelArguments/CommandUsageWriter.cs b/src/Samples/TopLevelArguments/CommandUsageWriter.cs new file mode 100644 index 00000000..5d95579c --- /dev/null +++ b/src/Samples/TopLevelArguments/CommandUsageWriter.cs @@ -0,0 +1,41 @@ +using Ookii.CommandLine; + +namespace TopLevelArguments; + +// Custom usage writer used for commands. +class CommandUsageWriter : UsageWriter +{ + // This lets us exclude the command usage syntax when appending command usage with the + // TopLevelUsageWriter, and include it when we're running a command and may need to show usage + // for that. + public bool IncludeCommandUsageSyntax { get; set; } + + // Indicate there are global arguments in the command usage syntax. + protected override void WriteUsageSyntaxPrefix() + { + WriteColor(UsagePrefixColor); + Write("Usage: "); + ResetColor(); + Write(' '); + Write(ExecutableName); + Writer.Write(" [global arguments]"); + if (CommandName != null) + { + Write(' '); + Write(CommandName); + } + } + + // Omit the usage syntax when writing the command list after the top-level usage help. + protected override void WriteCommandListUsageSyntax() + { + if (IncludeCommandUsageSyntax) + { + base.WriteCommandListUsageSyntax(); + } + } + + // Also include the global arguments in the help instruction. + protected override void WriteCommandHelpInstruction(string name, string argumentNamePrefix, string argumentName) + => base.WriteCommandHelpInstruction(name + " [global arguments]", argumentNamePrefix, argumentName); +} diff --git a/src/Samples/TopLevelArguments/EncodingConverter.cs b/src/Samples/TopLevelArguments/EncodingConverter.cs new file mode 100644 index 00000000..4a80526e --- /dev/null +++ b/src/Samples/TopLevelArguments/EncodingConverter.cs @@ -0,0 +1,24 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Conversion; +using System.Globalization; +using System.Text; + +namespace TopLevelArguments; + +// An ArgumentConverter for the Encoding class, using the utility base class provided by +// Ookii.CommandLine. +internal class EncodingConverter : ArgumentConverter +{ + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + { + try + { + return Encoding.GetEncoding(value); + } + catch (ArgumentException ex) + { + // This is the expected exception type for a converter. + throw new FormatException(ex.Message, ex); + } + } +} diff --git a/src/Samples/TopLevelArguments/ExitCode.cs b/src/Samples/TopLevelArguments/ExitCode.cs new file mode 100644 index 00000000..4d032c00 --- /dev/null +++ b/src/Samples/TopLevelArguments/ExitCode.cs @@ -0,0 +1,10 @@ +namespace TopLevelArguments; + +// Constants for exit codes used by this sample. +internal enum ExitCode +{ + Success, + CreateCommandFailure, + ReadWriteFailure, + FileExists, +} diff --git a/src/Samples/TopLevelArguments/GeneratedManager.cs b/src/Samples/TopLevelArguments/GeneratedManager.cs new file mode 100644 index 00000000..66780684 --- /dev/null +++ b/src/Samples/TopLevelArguments/GeneratedManager.cs @@ -0,0 +1,9 @@ +using Ookii.CommandLine.Commands; + +namespace TopLevelArguments; + +// Use source generation to locate commands in this assembly. +[GeneratedCommandManager] +partial class GeneratedManager +{ +} diff --git a/src/Samples/TopLevelArguments/Program.cs b/src/Samples/TopLevelArguments/Program.cs new file mode 100644 index 00000000..2445ae70 --- /dev/null +++ b/src/Samples/TopLevelArguments/Program.cs @@ -0,0 +1,73 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Terminal; + +[assembly: ApplicationFriendlyName("Ookii.CommandLine Top-level Arguments Sample")] + +namespace TopLevelArguments; + +static class Program +{ + static async Task Main() + { + // Modified usage format for the command list and commands to account for global arguments. + var commandUsageWriter = new CommandUsageWriter(); + + // 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 commandOptions = new CommandOptions() + { + IsPosix = true, + // The top-level arguments will have a -Version argument, so no need for a version + // command. + AutoVersionCommand = false, + UsageWriter = commandUsageWriter, + }; + + var manager = new GeneratedManager(commandOptions); + + // Use different options for the top-level arguments. + var parseOptions = new ParseOptions() + { + IsPosix = true, + // Modified usage format to list commands as well as top-level usage. + UsageWriter = new TopLevelUsageWriter(manager) + }; + + // First parse the top-level arguments. + var parser = TopLevelArguments.CreateParser(parseOptions); + Arguments = parser.ParseWithErrorHandling(); + if (Arguments == null) + { + return (int)ExitCode.CreateCommandFailure; + } + + // Run the command indicated in the top-level --command argument, and pass along the + // arguments that weren't consumed by the top-level CommandLineParser. + commandUsageWriter.IncludeCommandUsageSyntax = true; + return await manager.RunCommandAsync(Arguments.Command, parser.ParseResult.RemainingArguments) + ?? (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); + } + } + + // Provides access to the top-level arguments for use by the commands. + public static TopLevelArguments? Arguments { get; private set; } +} diff --git a/src/Samples/TopLevelArguments/README.md b/src/Samples/TopLevelArguments/README.md new file mode 100644 index 00000000..d50c1bf7 --- /dev/null +++ b/src/Samples/TopLevelArguments/README.md @@ -0,0 +1,86 @@ +# Subcommands with top-level arguments sample + +This sample shows an alternative way to define arguments that are common to every command. Rather +than using a base class with the common arguments, which makes the common arguments part of each +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 +more detailed descriptions. This sample uses POSIX conventions, for variation, but this isn't +required. + +These [top-level arguments](TopLevelArguments.cs) include a required positional argument that +indicates the command name to run. The argument uses [`CancelMode.Success`][] so that parsing will stop +at that point, while still returning success. The main function can then run that command, and pass +the remaining arguments to the command. + +The sample also customizes the usage help in two ways. The [`TopLevelUsageWriter`](TopLevelUsageWriter.cs) +is used for the top-level arguments themselves. It alters the usage syntax to show the positional +arguments last, and to indicate additional command-specific arguments can follow them. It also +shows the command list after the usage help for the arguments. + +The [`CommandUsageWriter`](CommandUsageWriter.cs) is used for the command manager and the commands +themselves. It is used to disable the command list usage syntax when writing the command list as part +of the top-level usage help, and to include text in the syntax to indicate there are additional +global arguments. + +This means we get the following if running `./TopLevelArguments -Help`: + +```text +Subcommands with top-level arguments sample for Ookii.CommandLine. + +Usage: TopLevelArguments [--encoding ] [--help] [--version] [--path] + [--command] [command arguments] + + --path + The path of the file to read or write. + + --command + The command to run. After this argument, all remaining arguments are passed to the + command. + + -e, --encoding + The encoding to use to read the file. The default value is utf-8. + + -?, --help [] (-h) + Displays this help message. + + --version [] + Displays version information. + +The following commands are available: + + read + Reads and displays data from a file using the specified encoding, wrapping the text to fit + the console. + + write + Writes lines to a file, wrapping them to the specified width. + +Run 'TopLevelArguments [global arguments] --help' for more information about a command. +``` + +And the following if we run `./TopLevelArguments somefile.txt write -Help` + +```text +Writes lines to a file, wrapping them to the specified width. + +Usage: TopLevelArguments [global arguments] write [[--lines] ...] [--help] + [--maximum-line-length ] [--overwrite] + + --lines + The lines of text to write to the file; if no lines are specified, this application will + read from standard input instead. + + -?, --help [] (-h) + Displays this help message. + + -m, --maximum-line-length + The maximum length of the lines in the file, or 0 to have no limit. Must be at least 0. + Default value: 79. + + -o, --overwrite [] + 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 diff --git a/src/Samples/TopLevelArguments/ReadCommand.cs b/src/Samples/TopLevelArguments/ReadCommand.cs new file mode 100644 index 00000000..3821cc7b --- /dev/null +++ b/src/Samples/TopLevelArguments/ReadCommand.cs @@ -0,0 +1,44 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Commands; +using System.ComponentModel; + +namespace TopLevelArguments; + +// This command is identical to the read command of the Subcommand sample; see that for a more +// detailed description. +[GeneratedParser] +[Command] +[Description("Reads and displays data from a file using the specified encoding, wrapping the text to fit the console.")] +partial class ReadCommand : AsyncCommandBase +{ + // Run the command after the arguments have been parsed. + public override async Task RunAsync() + { + try + { + // We use a LineWrappingTextWriter to neatly wrap console output + using var writer = LineWrappingTextWriter.ForConsoleOut(); + + // Write the contents of the file to the console. + await foreach(var line in File.ReadLinesAsync(Program.Arguments!.Path.FullName, Program.Arguments.Encoding)) + { + await writer.WriteLineAsync(line); + } + + // 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. + return (int)ExitCode.Success; + } + catch (IOException ex) + { + Program.WriteErrorMessage(ex.Message); + return (int)ExitCode.ReadWriteFailure; + } + catch (UnauthorizedAccessException ex) + { + Program.WriteErrorMessage(ex.Message); + return (int)ExitCode.ReadWriteFailure; + } + } +} diff --git a/src/Samples/TopLevelArguments/TopLevelArguments.cs b/src/Samples/TopLevelArguments/TopLevelArguments.cs new file mode 100644 index 00000000..790dbaa4 --- /dev/null +++ b/src/Samples/TopLevelArguments/TopLevelArguments.cs @@ -0,0 +1,34 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Conversion; +using System.ComponentModel; +using System.Text; + +namespace TopLevelArguments; + +[GeneratedParser] +[Description("Subcommands with top-level arguments sample for Ookii.CommandLine.")] +partial class TopLevelArguments +{ + // A required, positional argument to specify the file name. + [CommandLineArgument(IsPositional = true)] + [Description("The path of the file to read or write.")] + public required FileInfo Path { get; set; } + + // A required, positional argument to specify what command to run. + // + // When this argument is encountered, parsing is canceled, returning success using the arguments + // so far. The Main() method will then pass the remaining arguments to the specified command. + [CommandLineArgument(IsPositional = true, CancelParsing = CancelMode.Success)] + [Description("The command to run. After this argument, all remaining arguments are passed to the command.")] + public required string Command { get; set; } + + // An argument to specify the encoding. + // Because Encoding doesn't have a default ArgumentConverter, we use a custom one provided in + // this sample. + // Encoding's ToString() implementation just gives the class name, so don't include the default + // value in the usage help; we'll write it ourself instead. + [CommandLineArgument(IsShort = true, IncludeDefaultInUsageHelp = false)] + [Description("The encoding to use to read the file. The default value is utf-8.")] + [ArgumentConverter(typeof(EncodingConverter))] + public Encoding Encoding { get; set; } = Encoding.UTF8; +} diff --git a/src/Samples/TopLevelArguments/TopLevelArguments.csproj b/src/Samples/TopLevelArguments/TopLevelArguments.csproj new file mode 100644 index 00000000..f93e0e0a --- /dev/null +++ b/src/Samples/TopLevelArguments/TopLevelArguments.csproj @@ -0,0 +1,25 @@ + + + + Exe + net7.0 + enable + enable + Subcommands with top-level arguments sample for Ookii.CommandLine. + Copyright (c) Sven Groot (Ookii.org) +This is sample code, so you can use it freely. + true + + + + + + + + + diff --git a/src/Samples/TopLevelArguments/TopLevelUsageWriter.cs b/src/Samples/TopLevelArguments/TopLevelUsageWriter.cs new file mode 100644 index 00000000..ef9b4bed --- /dev/null +++ b/src/Samples/TopLevelArguments/TopLevelUsageWriter.cs @@ -0,0 +1,35 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Commands; + +namespace TopLevelArguments; + +// Custom UsageWriter used for the top-level arguments. +internal class TopLevelUsageWriter : UsageWriter +{ + private readonly CommandManager _manager; + + public TopLevelUsageWriter(CommandManager manager) + { + _manager = manager; + } + + // Show the positional arguments last to indicate arguments after --command must be command + // arguments. + protected override IEnumerable GetArgumentsInUsageOrder() + => Parser.Arguments + .Where(a => a.Position == null) + .Concat(Parser.Arguments.Where(a => a.Position != null)); + + // Indicate command arguments can follow the --command argument. + protected override void WriteUsageSyntaxSuffix() + { + Writer.Write(" [command arguments]"); + } + + // Write the command list at the end of the usage. + protected override void WriteArgumentDescriptions() + { + base.WriteArgumentDescriptions(); + _manager.WriteUsage(); + } +} diff --git a/src/Samples/TopLevelArguments/WriteCommand.cs b/src/Samples/TopLevelArguments/WriteCommand.cs new file mode 100644 index 00000000..642c540c --- /dev/null +++ b/src/Samples/TopLevelArguments/WriteCommand.cs @@ -0,0 +1,100 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Validation; +using System.ComponentModel; + +namespace TopLevelArguments; + +// This command is identical to the write command of the Subcommand sample; see that for a more +// detailed description. +[GeneratedParser] +[Command] +[Description("Writes lines to a file, wrapping them to the specified width.")] +partial class WriteCommand : AsyncCommandBase +{ + // Positional multi-value argument to specify the text to write + [CommandLineArgument(IsPositional = true)] + [Description("The lines of text to write to the file; if no lines are specified, this application will read from standard input instead.")] + public string[]? Lines { get; set; } + + // An argument that specifies the maximum line length of the output. + [CommandLineArgument(IsShort = true)] + [Description("The maximum length of the lines in the file, or 0 to have no limit.")] + [ValidateRange(0, null)] + public int MaximumLineLength { get; set; } = 79; + + // A switch argument that indicates it's okay to overwrite files. + [CommandLineArgument(IsShort = true)] + [Description("When this option is specified, the file will be overwritten if it already exists.")] + public bool Overwrite { get; set; } + + // Run the command after the arguments have been parsed. + public override async Task RunAsync() + { + try + { + // Check if we're allowed to overwrite the file. + if (!Overwrite && Program.Arguments!.Path.Exists) + { + // 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."); + return (int)ExitCode.FileExists; + } + + var options = new FileStreamOptions() + { + Access = FileAccess.Write, + Mode = Overwrite ? FileMode.Create : FileMode.CreateNew, + Share = FileShare.ReadWrite | FileShare.Delete, + Options = FileOptions.Asynchronous + }; + + using var writer = new StreamWriter(Program.Arguments!.Path.FullName, Program.Arguments.Encoding, options); + + // We use a LineWrappingTextWriter to neatly white-space wrap the output. + using var lineWriter = new LineWrappingTextWriter(writer, MaximumLineLength); + + // Write the specified content to the file + foreach (string line in GetLines()) + { + await lineWriter.WriteLineAsync(line); + } + + return (int)ExitCode.Success; + } + catch (IOException ex) + { + Program.WriteErrorMessage(ex.Message); + return (int)ExitCode.ReadWriteFailure; + } + catch (UnauthorizedAccessException ex) + { + Program.WriteErrorMessage(ex.Message); + return (int)ExitCode.ReadWriteFailure; + } + } + + private IEnumerable GetLines() + { + // Choose between the specified lines or standard input. + if (Lines == null || Lines.Length == 0) + { + return EnumerateStandardInput(); + } + + return Lines; + } + + private static IEnumerable EnumerateStandardInput() + { + // Read from standard input. You can pipe a file to the input, or use it interactively (in + // that case, press CTRL-D (CTRL-Z on Windows) to send an EOF character and stop writing). + string? line; + while ((line = Console.ReadLine()) != null) + { + yield return line; + } + } +} diff --git a/src/Samples/Wpf/App.xaml.cs b/src/Samples/Wpf/App.xaml.cs index d1d97fc5..a5408888 100644 --- a/src/Samples/Wpf/App.xaml.cs +++ b/src/Samples/Wpf/App.xaml.cs @@ -11,9 +11,10 @@ public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { - // Use the CommandLineParser class, instead of the static CommandLineParser.Parse - // method, so we can manually handle errors. - var parser = new CommandLineParser(); + // Use the CommandLineParser class, instead of the static Parse() method, so we can + // manually handle errors. The GeneratedParserAttribute generates a CreateParser() method + // for this purpose. + var parser = Arguments.CreateParser(); try { var args = parser.Parse(e.Args); diff --git a/src/Samples/Wpf/Arguments.cs b/src/Samples/Wpf/Arguments.cs index 8054b033..bf187832 100644 --- a/src/Samples/Wpf/Arguments.cs +++ b/src/Samples/Wpf/Arguments.cs @@ -8,9 +8,10 @@ namespace WpfSample; // This class defines the arguments for the sample. It uses the same arguments as the Parser // sample, so see that sample for more detailed descriptions. +[GeneratedParser] [ApplicationFriendlyName("Ookii.CommandLine WPF Sample")] [Description("Sample command line application for WPF. The application parses the command line and shows the results, but otherwise does nothing and none of the arguments are actually used for anything.")] -public class Arguments +public partial class Arguments { // The automatic version argument writes to the console, which is not useful in a WPF // application. Instead, we define our own, which shows the same information in a dialog. @@ -23,7 +24,7 @@ public class Arguments // (as in this case), it defaults to a switch argument. [CommandLineArgument] [Description("Displays version information.")] - public static bool Version(CommandLineParser parser) + public static CancelMode Version(CommandLineParser parser) { var assembly = Assembly.GetExecutingAssembly(); @@ -44,26 +45,27 @@ public static bool Version(CommandLineParser parser) // Indicate parsing should be canceled and the application should exit. Because we didn't // set the CommandLineParser.HelpRequested property, usage help will not be shown. - return false; + return CancelMode.Abort; } - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The source data.")] - public string Source { get; set; } = string.Empty; + public required string Source { get; set; } - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The destination data.")] - public string Destination { get; set; } = string.Empty; + public required string Destination { get; set; } - [CommandLineArgument(Position = 2, DefaultValue = 1)] + [CommandLineArgument(IsPositional = true)] [Description("The operation's index.")] - public int OperationIndex { get; set; } + public int OperationIndex { get; set; } = 1; [CommandLineArgument] [Description("Provides a date to the application.")] public DateTime? Date { get; set; } - [CommandLineArgument(ValueDescription = "Number")] + [CommandLineArgument] + [ValueDescription("Number")] [Description("Provides the count for something to the application.")] [ValidateRange(0, 100)] public int Count { get; set; } diff --git a/src/Samples/Wpf/README.md b/src/Samples/Wpf/README.md index 6df42580..057fbe6a 100644 --- a/src/Samples/Wpf/README.md +++ b/src/Samples/Wpf/README.md @@ -5,9 +5,10 @@ interface. It uses the same arguments as the [parser sample](../Parser). Running this sample requires Microsoft Windows. -This sample does not use the static [`CommandLineParser.Parse()`][] method, but instead handles -errors manually so it can show a dialog with the error message and a help button, and show the -usage help only if that button was clicked, or the `-Help` argument was used. +This sample does not use the generated static [`Parse()`][Parse()_7] method, but instead uses the generated +[`CreateParser()`][CreateParser()_1] method, and handles errors manually so it can show a dialog with the error message +and a help button, and show the usage help only if that button was clicked, or the `-Help` argument +was used. To use as much of the built-in usage help generation as possible, this sample uses a class derived from the [`UsageWriter`][] class (see [HtmlUsageWriter.cs](HtmlUsageWriter.cs)) that wraps the @@ -20,8 +21,8 @@ The sample uses a simple CSS stylesheet to format the usage help; you can make t like, of course. This is by no means the only way. Since all the information needed to generate usage help is -available in the [`CommandLineParser`][] class, you could for example use a custom XAML page to show -the usage help. +available in the [`CommandLineParser`][] class, you could for example use a custom XAML page to +show the usage help. This sample also defines a custom `-Version` argument; the automatic one that gets added by Ookii.CommandLine writes to the console, so it isn't useful here. This manual implementation shows @@ -32,6 +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.Parse()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm +[`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 diff --git a/src/Samples/Wpf/Wpf.csproj b/src/Samples/Wpf/Wpf.csproj index a9867414..4bc32915 100644 --- a/src/Samples/Wpf/Wpf.csproj +++ b/src/Samples/Wpf/Wpf.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows + net7.0-windows enable true true @@ -14,13 +14,18 @@ This is sample code, so you can use it freely. - + + + diff --git a/src/Snippets/CSharp/clargclass.snippet b/src/Snippets/CSharp/clargclass.snippet index 19e9534a..7b0890d0 100644 --- a/src/Snippets/CSharp/clargclass.snippet +++ b/src/Snippets/CSharp/clargclass.snippet @@ -46,18 +46,14 @@ System.ComponentModel - (); - } + // public required string SampleArgument { get; set; } }]]> diff --git a/src/Snippets/CSharp/clargpos.snippet b/src/Snippets/CSharp/clargpos.snippet index d0426de8..68c24ffb 100644 --- a/src/Snippets/CSharp/clargpos.snippet +++ b/src/Snippets/CSharp/clargpos.snippet @@ -17,20 +17,6 @@ - - Position - Argument position - 0 - - - - - Required - Whether the argument is required - false - - - Name Property and argument name @@ -67,7 +53,7 @@ System.ComponentModel - diff --git a/src/Snippets/CSharp/clargreq.snippet b/src/Snippets/CSharp/clargreq.snippet new file mode 100644 index 00000000..d75a4b29 --- /dev/null +++ b/src/Snippets/CSharp/clargreq.snippet @@ -0,0 +1,61 @@ + + + +
+ + Expansion + + clargreq + Sven Groot (Ookii.org) + + Snippet for creating a required positional command line argument for use with Ookii.CommandLine. + + + https://github.com/SvenGroot/ookii.commandline + + clargreq +
+ + + + Name + Property and argument name + MyArgument + + + + + Type + Property and argument type + string + + + + + Description + The argument description + Argument description. + + + + + + + Ookii.CommandLine.dll + https://github.com/SvenGroot/ookii.commandline + + + + + Ookii.CommandLine + + + System.ComponentModel + + + + +
+
\ No newline at end of file diff --git a/src/Snippets/CSharp/clcmd.snippet b/src/Snippets/CSharp/clcmd.snippet index 1167f2b1..d4c9baf9 100644 --- a/src/Snippets/CSharp/clcmd.snippet +++ b/src/Snippets/CSharp/clcmd.snippet @@ -56,13 +56,14 @@ System.ComponentModel - System.ComponentModel - Run() { diff --git a/src/Snippets/Ookii.CommandLine.Snippets.csproj b/src/Snippets/Ookii.CommandLine.Snippets.csproj index acbdfa36..1a5c2a3b 100644 --- a/src/Snippets/Ookii.CommandLine.Snippets.csproj +++ b/src/Snippets/Ookii.CommandLine.Snippets.csproj @@ -122,6 +122,11 @@ OokiiCommandLineVB true + + Always + true + OokiiCommandLineCS + Designer diff --git a/src/Snippets/README.md b/src/Snippets/README.md new file mode 100644 index 00000000..a4c72150 --- /dev/null +++ b/src/Snippets/README.md @@ -0,0 +1,3 @@ +# Code snippets + +See the [code snippets documentation](../../docs/CodeSnippets.md). diff --git a/src/Snippets/Readme.txt b/src/Snippets/Readme.txt deleted file mode 100644 index bcd0a4a7..00000000 --- a/src/Snippets/Readme.txt +++ /dev/null @@ -1,23 +0,0 @@ -Code snippets for Ookii.CommandLine ------------------------------------ - -Several code snippets are provided to make working with Ookii.CommandLine -even easier: - -clargclass: Snippet for am argument class, including a static Create method - that parses arguments. - -clarg: Snippet for a command line argument. - -clargpos: Snippet for a positional command line argument. - -clargmulti: Snippet for a multi-value command line argument. - -clargdict: Snippet for a dictionary command line argument. - -All snippets are provided for C# and Visual Basic. To use them, install the VSIX -extension, or manually copy the snippet files to the -"Visual Studio \Code Snippets\Visual C#\My Code Snippets" or -"Visual Studio \Code Snippets\Visual Basic\My Code Snippets" folder -located in your Documents folder. Alternatively, use the snippet manager -inside Visual Studio to import the snippet files. diff --git a/src/Snippets/source.extension.vsixmanifest b/src/Snippets/source.extension.vsixmanifest index e869ba7e..6375959c 100644 --- a/src/Snippets/source.extension.vsixmanifest +++ b/src/Snippets/source.extension.vsixmanifest @@ -1,10 +1,10 @@ - + Ookii.CommandLine.Snippets - Code snippets for defining and parsing command line arguments using the Ookii.CommandLine library (version 3.0 or later). Supports C# and Visual Basic. - https://www.github.com/SvenGroot/ookii.commandline + Code snippets for defining and parsing command line arguments using the Ookii.CommandLine library (version 4.0 or later). Supports C# and Visual Basic. + https://www.github.com/SvenGroot/Ookii.Commandline license.txt